OvermindDL1

OvermindDL1

Just got an idea on how we could get a strongly typed elixir ‘now’. Imagine this:

import TypedElixir
defmodulet TypedTest do
  @moduledoc false

  @spec hello() :: String.t
  def hello, do: "world"

end

Or whatever for a name instead of defmodulet (I’m horrible with names) right now it is just a typed version of defmodule. It could start by enforcing @specs, then another pass to ensure that the inner function calls also follow the spec of the function by checking the specs of the other calls. Any calls that are not @specd outside of the enforced defmodulet bounds would require some type of @spec elsewhere. You could also @spec variables inside a function if so wished (better debugging to make sure you are passing things around right), but otherwise make sure that they are used properly in the function. If your spec does not match the def/defp usage then it would error, giving both what it detects it should be and what it is.

Even if I have to @spec everything in my program, I would so far beyond love getting compiler errors for mis-using types. Absolutely requiring @spec on def/defp when within a defmodulet makes type inference within the function significantly easier to reason about both for the coder and for the TypedElixir library.

It would indeed by quite nice if such type checking was added to the base defmodule, we could even pass in a @strict_types as a module attribute or so to enforce the above (require accurate @specs, not too generic, etc… etc…) but otherwise backwards compatible to now but with occasional helper messages at compile-time like This will always fail as you cannot add an integer and a string as these bindings will always be an integer and a string or so. A default compile would not cross-module type-check unless a special flag would be added or @strict_types were specified or so, which would then cause the compiler to load the other modules to acquire their typespecs, which could increase compiling time admittedly, but only one level deep may not be noticable.

My motivation for this is 95% of my bugs in Elixir/Erlang are due to using types wrong, like I may slightly change a tuple format somewhere but do not update it elsewhere and dialyzer does not catch it because the prior library state was in its cache that I then need to rebuild, in addition to dialyzer can take a long time to run. And honestly I just do not want an incorrect program to compile at all, I want it to be noisy and fail at compile-time, not run-time. Even a little bit of extra checking then would save so much pain.

Either-way, I made a TypedElixir library of the above, only thing it does so far is check that @specs exist on each def/defp as I play around with it (literally mix new’d it <5 minutes before this post), does not expand macros first or anything yet (should probably be next step). I’m curious on ideas on if this is a good idea or if I should not bother with the effort?

EDIT0: The expansion and some clean-up done, still only checking that @specs exist, nothing else yet…

Showing Posts 1 to 10

OvermindDL1

OvermindDL1 OP

Given this module:

    defmodulet TypedTest do
      @moduledoc false

      import String

      @type test_type :: String.t

      @spec simple() :: nil
      def simple(), do: nil

      @spec hello(String.t) :: String.t | Map.t
      def hello(str) when is_binary(str) do
        # @spec ret :: String.t # TODO
        ret = trim(str) <> " world"
        ret
      end

      def fun_no_spec(), do: nil
    end

So far it just tests if specs exist, but it is a start. Verbosely it prints out this at compile-time:

Type Checking: TypedElixirTest.TypedTest

Types:
%{test_type: {{:., [line: 19],
    [{:__aliases__, [counter: 0, line: 19], [:String]}, :t]}, [line: 19], []}}

Specs:
%{{:hello,
   1} => {[{{:., [line: 24],
      [{:__aliases__, [counter: 0, line: 24], [:String]}, :t]}, [line: 24],
     []}],
   {:|, [line: 24],
    [{{:., [line: 24], [{:__aliases__, [counter: 0, line: 24], [:String]}, :t]},
      [line: 24], []},
     {{:., [line: 24], [{:__aliases__, [counter: 0, line: 24], [:Map]}, :t]},
      [line: 24], []}]}}, {:simple, 0} => {[], nil}}

Funs:
%{{:fun_no_spec, 0} => {{:fun_no_spec, [line: 31], []}, [do: nil]},
  {:hello,
   1} => {{:when, [line: 25],
    [{:hello, [line: 25], [{:str, [line: 25], nil}]},
     {:is_binary, [line: 25], [{:str, [line: 25], nil}]}]},
   [do: {:__block__, [line: 12],
     [{:=, [line: 27],
       [{:ret, [line: 27], nil},
        {:<>, [line: 27],
         [{:trim, [line: 27], [{:str, [line: 27], nil}]}, " world"]}]},
      {:ret, [line: 28], nil}]}]},
  {:simple, 0} => {{:simple, [line: 22], []}, [do: nil]}}

Funs missing specs:
[{{:fun_no_spec, 0}, {{:fun_no_spec, [line: 31], []}, [do: nil]}}]

Finished in 0.09 seconds

I wonder if @types can have arguments to make them parameterized… if not I should support that if possible or make a new one… I do not have too much time to work on this but at least it is a start if I confine my code to a certain style…

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Typing is hard.

@spec foo :: string
def foo() do
  receive do
    x -> x
  end
end

I don’t know how you’re gonna handle stuff like ^

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Types can be parameterized in the following way:

@spec reduce(list, b, (a, b -> b)) :: b when a: any, b: any
OvermindDL1

OvermindDL1 OP

That is the challenge, I am curious if it is even possible in Elixir. So unless someone finds a case that is truly beyond difficult or impossible, I may work on it little by little over time. :slight_smile:

Oh, and in that case with receive, I’d try to throw some error like (in the optimal case):

Function: Module.foo
Location: file.ex:76
Must return a type of: string
However it returns a type of: any()
Suggestion:  Change or add a case from receive that adds a `when is_binary` condition

Or something like that. I want to be forced to put typing on anything and everything that is ambiguous, that is how you catch a lot of bugs. :slight_smile:

OvermindDL1

OvermindDL1 OP

Been playing with more, slowly building a DHM inference engine with dependent types (mostly a learning exercise), not working yet but would be nice to get something like this:

defmodule Testing do
  @spec div(number, (d is number if d != 0)) :: number
  def div(x, y), do: x / y
end

Not my preferred syntax but the Elixir parser is unforgiving for what I would prefer (without resorting to strings, blehg). This would define a Testing.div/2 function that accepts any number in its first argument, any number that is not 0 in its second, and can return any number. Basically if you tried to do something like:

case Integer.parse(getInputFromUser()) do
  {i, ""} -> Testing.div(40, i) # Boom
  _ -> return nil
end

Then on the line with the Boom comment it would fail to compile due to unmatched constraint or so, you would have to do something like this instead:

case Int.parse(getInputFromUser()) do
  {i, ""} when i != 0 -> Testing.div(40, i) # Boom
  _ -> return 0
end

Or something that would actively refine the constraints of i to not include 0.

I doubt I will finish this, I think I’d be more apt to write an OCaml backend to Elixir, but this is still a fun very-slow-moving-project. ^.^

EDIT: Yeesh, this was two months later? I really do have about no time… Wish I could get paid to do this. >.>

gon782

gon782

@spec foo :: received(t) # maybe received(string)?
def foo() do
  receive do
    x -> x
    end
end
sashaafm

sashaafm

How would you go about to writing an OCaml backend to Elixir? That idea seems very interesting, but I don’t see how that would bring static typing to Elixir?

OvermindDL1

OvermindDL1 OP

It would be to supplement Elixir. Any module you made in OCaml would output an Elixir module of the same name. You could call into it from normal Elixir code, and you could call normal Elixir code from the OCaml-output-version by the ‘external’ declaration, that is the easy stuff. Everything within a module you’d know would be typed-safe, so as long as the external Elixir modules call it right (and I could always add when checks and assertions and such at public points to verify) then no worry about something stupid within (like me passing a user object in the room field in one of my projects here…). Could slowly convert your code to OCaml or just add it as you go. The more you’d have, the safer the overall project would be.

OCaml itself does not have dependent types (few ML languages do, Haskell does not either), but you can emulate them via typed modules and probably GADT’s… Just playing with the idea of them in my playground here. :slight_smile:

DianaOlympos

DianaOlympos

Just so you know, the problem Philip Wadler found when he tried to type erlang :

  • message
  • pid and process in particular self ()

The other question is… how do you deal with distributed message. You can get a message from a node that do not follow the comtract you assigned. So your type checking is useless in that case…

michalmuskala

michalmuskala

Another thing that makes typing erlang/elixir really difficult is dynamic loading of modules. The function you’re using may actually not exist yet when you’re writing it, the module may be loaded later. Not to mention hot upgrades that completely mess stuff up.

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 92995 915
New
caslu
I want to open this thread for you all to discuss and help those who really like Ash but are still hesitant to use it in a real project. ...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
GES233
I’m posting this in response to Jose’s recent tweet (Cr. link) : People are sleeping on Elixir for a coding harness: Hot-code swappi...
New
_mfierro
Hello, I wrote Stop My Hand, a Scattergories-like web application using Phoenix/LiveView as my learning project for Elixir (after readin...
New
marciol
It would be helpful to have a list of companies worldwide that hire engineers without prior experience in Elixir. Often, it can be quite ...
New
durvia
Anyone running long-lived stateful processes on BEAM? We’re building an AI agent runtime and would love to compare notes. We’re a small ...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews