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

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
achempion
I’ve been using Emacs as my main code editor for more than a two years. It’s a custom build version although I’ve tried doom emacs and sp...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
jtormey
Lately I’ve been thinking about how to organize components as a LiveView application grows. One of the pain points I’ve found (for myself...
New
Null-logic-0
What IDE or editor are you using for Elixir development? Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
New

Other Trending Topics Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
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
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews