Typed Elixir

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…

20 Likes

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…

1 Like

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 ^

2 Likes

Types can be parameterized in the following way:

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

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:

6 Likes

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. >.>

6 Likes
@spec foo :: received(t) # maybe received(string)?
def foo() do
  receive do
    x -> x
    end
end
1 Like

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?

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:

2 Likes

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…

1 Like

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.

3 Likes

The papers on dialyzer and success types include a lot of the pitfalls in trying to build an ML type system for Erlang: https://it.uu.se/research/group/hipe/papers/succ_types.pdf.

@OvermindDL1 Have you looked into multi-party session types? http://groups.inf.ed.ac.uk/abcd/index.html. That research seems like it could be a promising way forward.

1 Like

Already have a test library for that (in OCaml). A quick overview:
Message is just typed as Erlang.t, which is a blackbox unknown erlang type.
Pid is an Erlang.Pid.t, which is also a blackbox erlang type that you know is a pid, you can do something like Erlang.Pid.send somePid someMessage as an example.

The Erlang module would have testing like Erlang.is_int something as well as reifiers like Erlang.reify something that can be used like:

let open Erlang in
match reify something with
| Int i -> doSomethingWithInt i
| Binary b -> doSomethingWithBinary b
| Tuple [ Int i; Float f ] -> doSomethingWithIntAndFloat i f
| _ -> doSomethingAsDefaultCase

Or so forth, and those are only the primitives, can build up better matchers on top of that pretty easily (including dynamically creating arbitrary depth decoders based on types of, say, a receive call). This is pretty easy to do in OCaml and I already have this part done (as I was playing around to see how easy it is). I was planning something similar in this once I figured up a good API but I do think I’d end up having to enforce a new language or so…

Eyup, this would only typecheck at compile-time, at runtime it would all still be elixir and you gotta deal with the changes you make, if you do non-atomic hot code swapping and you change your API, then just like in normal erlang be prepared to deal with the consequences (and exceptions, :EXIT’s, etc…). This is less to ensure safety at runtime (erlang does that pretty well already) and more to help ensure that I do not screw up at compile-time by passing things in the wrong order or so, that is my primary use of statically typed languages. Trying to statically type it all at runtime is a fools errand and not something I would even attempt except for occasionally throwing in is_int’s or so in when clauses. I want something to help me code, not ensure safety at runtime (as stated, erlang does that well enough itself via OTP).

Yeah I ran across that long ago, and is one of the reasons I started by thinking of a way to black-box a type like my above Erlang.t type, that way I can always fall back to that and force the user (I.E. me) to do proper tests to reify it and use what they want and handle the cases where they get what they do not want.

Hmm, that is not something I’ve come across, seems like a decoder of the Erlang.t type could fit within that, it either succeeds or fails given a pattern, which is defined manually if you wish like:

let myDecoder =
  let open Erlang.Decoders in
  Tuple
    [ Literal.Atom `Something
    ; Atom
    ; Validate Int (fun i -> i >= 0 && i < 10)
    ; Binary
    ]

(* Use it like: *)
match Erlang.Decoders.decode myDecoder something with
| Some (_, someAtom, someInt, someBinary) -> doSomething someAtom someInt someBinary
| Error _e -> ()

Which in Elixir would be the same as:

case something do
  (:Something, someAtom, someInt, someBinary) when
    is_atom(someAtom) and
    is_int(someInt) and
    is_binary(someBinary) and
    someInt>=0 and someInt<10 ->
      doSomething(someAtom, someInt, someBinary)
  _ -> nil
end

Thus modules can define decoders (and with mapping and such can transform the data as well during decoding) that can be used elsewhere to parse data. If I made an ocaml backend (I would so love to get the time for that!) then reify could reify a whole tree so you could match on something like this as well:

let rec blah something v in
  let open Erlang in
  match Erlang.reify something with
  | Tuple [ Atom `Something; Int i ] -> i + v
  | List [] -> v
  | List (Int i :: rest) when i<10 -> blah rest (i + v)
  | _ -> v

Which in Elixir would be:

def blah something v do
  case something do
    (:Something, i) when is_int(i) -> i + v
    [] -> v
    [i | rest] when is_int(i) and i<10 -> blah(rest, v + i)
  end
end

In both cases it is just a function that can take a tuple of `(:Something, 42) or a list of integers that it will then sum up each element if the element is less than 10 (OCaml has ‘when’ like in Erlang/Elixir in its pattern matching too).

I’m pretty sure I’ve worked around each usual issue that would happen by having a statically typed language compile to Erlang/Elixir, although it would be slightly more wordy what with things like the Erlang.reify work and such. But for something like receive you can pass in a decoder (that can decode a variety of types, I modeled it on Elm’s JSON Decoders/Encoders) to get back a specific type or could just call it with an identity to get back the next message as an opaque type that you can then match on to unconditionally remove from the mailbox anyway.

An update after months!

So far these all work as expected, it uses types and spec’s when it can, and infer’s it otherwise when it cannot (or complains):

iex> use TypedElixir
TypedElixir

iex> defmodulet TypedTest_Empty do
...> end
{:module, TypedTest_Empty,
 <<70, 79, 82, 49, 0, 0, 3, 244, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 94,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, nil}

iex> defmodulet TypedTest_Typed_Simple do
...>   @spec simple() :: nil
...>   def simple(), do: nil
...> end
{:module, TypedTest_Typed_Simple,
 <<70, 79, 82, 49, 0, 0, 4, 196, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 129,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:simple, 0}}
iex> TypedTest_Typed_Simple.simple()
nil

iex> defmodulet TypedTest_Untyped_Simple do
...>   def simple(), do: nil
...> end
{:module, TypedTest_Untyped_Simple,
 <<70, 79, 82, 49, 0, 0, 4, 176, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 129,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:simple, 0}}
iex> TypedTest_Untyped_Simple.simple()
nil

iex> defmodulet TypedTest_Untyped_Recursive_Simple_BAD_NoSet do
...>   def simple(), do: simple()
...>   def willFail() do
...>     x = simple()
...>   end
...> end
** (throw) {:INVALID_ASSIGNMENT_NOT_ALLOWED, :no_return}
    (typed_elixir) lib/typed_elixir.ex:232: TypedElixir.type_check_expression/3
    (typed_elixir) lib/typed_elixir.ex:198: TypedElixir.type_check_def_body/3
    (typed_elixir) lib/typed_elixir.ex:121: TypedElixir.type_check_body/3
    (typed_elixir) lib/typed_elixir.ex:104: anonymous fn/3 in TypedElixir.typecheck_module/4
          (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
    (typed_elixir) lib/typed_elixir.ex:103: TypedElixir.typecheck_module/4
    (typed_elixir) expanding macro: TypedElixir.defmodulet/2
                   iex:5: (file)
iex> # Cannot set the return value of a function that never returns...
nil

iex> # The extra type is to give a name to the type in simple, so the input and output become the same type.
nil
iex> # If the spec was `simple(any()) :: any()` then you could not state that the output type is based on the input type.
nil
iex> defmodulet TypedTest_Typed_Identity do
...>   @type identity_type :: any()
...>   @spec identity(identity_type) :: identity_type
...>   def identity(x), do: x
...> end
{:module, TypedTest_Typed_Identity,
 <<70, 79, 82, 49, 0, 0, 5, 48, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 191,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Typed_Identity.identity(42)
42
iex>
nil

iex> defmodulet TypedTest_Typed_Identity_badtype do
...>   @type identity_type :: any()
...>   @spec identity(identity_type) :: identity_type
...>   def identity(_x), do: nil
...> end
** (throw) {:NO_TYPE_RESOLUTION, %TypedElixir.Type.Const{const: :atom, meta: %{values: [nil]}}, %TypedElixir.Type.Ptr.Generic{id: 0, named: true}}
    (typed_elixir) lib/typed_elixir.ex:514: TypedElixir.resolve_types_nolinks/3
    (typed_elixir) lib/typed_elixir.ex:454: TypedElixir.resolve_types!/3
    (typed_elixir) lib/typed_elixir.ex:168: TypedElixir.resolve_fun_return_type_/4
    (typed_elixir) lib/typed_elixir.ex:146: TypedElixir.resolve_fun_return_type/4
    (typed_elixir) lib/typed_elixir.ex:127: TypedElixir.type_check_body/3
    (typed_elixir) lib/typed_elixir.ex:104: anonymous fn/3 in TypedElixir.typecheck_module/4
          (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
    (typed_elixir) lib/typed_elixir.ex:103: TypedElixir.typecheck_module/4
iex> # Since it is a named type the input and output must match
nil

iex> defmodulet TypedTest_Typed_Identity_AnyReturn do
...>   @spec identity(any()) :: any()
...>   def identity(_x), do: nil
...> end
{:module, TypedTest_Typed_Identity_AnyReturn,
 <<70, 79, 82, 49, 0, 0, 4, 248, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 152,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Typed_Identity_AnyReturn.identity(42)
nil
iex> # An unnamed `any()` means it can literally return anything, the input does not matter
nil

iex> defmodulet TypedTest_Untyped_Identity do
...>   def identity(x), do: x
...> end
{:module, TypedTest_Untyped_Identity,
 <<70, 79, 82, 49, 0, 0, 4, 204, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 149,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Untyped_Identity.identity(42)
42

iex> defmodulet TypedTest_Untyped_Identity_AnyReturn do
...>   def identity(_x), do: nil
...> end
{:module, TypedTest_Untyped_Identity_AnyReturn,
 <<70, 79, 82, 49, 0, 0, 4, 236, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 152,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:identity, 1}}
iex> TypedTest_Untyped_Identity_AnyReturn.identity(42)
nil
iex> # Since it is not typed it gets an inferred return value of `nil`
nil

iex> defmodulet TypedTest_Typed_Recursive_Counter do
...>   @spec counter(integer()) :: integer()
...>   def counter(x), do: counter(x)
...> end
{:module, TypedTest_Typed_Recursive_Counter,
 <<70, 79, 82, 49, 0, 0, 4, 248, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 148,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:counter, 1}}
iex> # Not calling it because infinite recursion...
nil

iex> defmodulet TypedTest_Untyped_Recursive_Counter do
...>   def counter(x), do: counter(x)
...> end
{:module, TypedTest_Untyped_Recursive_Counter,
 <<70, 79, 82, 49, 0, 0, 4, 224, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 148,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:counter, 1}}
iex> # Again, not calling it because infinite recursion...

iex> defmodulet TypedTest_Typed_Recursive_Counter_Bad do
...>   @spec counter(integer()) :: integer()
...>   def counter(x), do: counter(6.28)
...> end
** (throw) {:NO_TYPE_UNIFICATION, :NO_PATH, %TypedElixir.Type.Const{const: :integer, meta: %{}}, %TypedElixir.Type.Const{const: :float, meta: %{values: [6.28]}}}
    (typed_elixir) lib/typed_elixir.ex:567: TypedElixir.unify_types_nolinks/3
    (typed_elixir) lib/typed_elixir.ex:521: TypedElixir.unify_types!/3
          (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
    (typed_elixir) lib/typed_elixir.ex:250: TypedElixir.type_check_expression/3
    (typed_elixir) lib/typed_elixir.ex:201: TypedElixir.type_check_def_body/3
    (typed_elixir) lib/typed_elixir.ex:121: TypedElixir.type_check_body/3
    (typed_elixir) lib/typed_elixir.ex:104: anonymous fn/3 in TypedElixir.typecheck_module/4
          (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3


iex> defmodulet TypedTest_Untyped_Recursive_Counter_RecallingDifferentType do
...>   def counter(_x), do: counter(6.28)
...> end
{:module, TypedTest_Untyped_Recursive_Counter_RecallingDifferentType,
 <<70, 79, 82, 49, 0, 0, 5, 56, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 0, 151,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>, {:counter, 1}}
iex> # Not calling this either, infinite recursion
nil

iex> defmodulet TypedTest_Typed_MultiFunc0 do
...>   @spec simple() :: nil
...>   def simple(), do: nil
...> 
...>   @type identity_type :: any()
...>   @spec identity(identity_type) :: identity_type
...>   def identity(x), do: x
...> 
...>   @spec call_simple(any()) :: any()
...>   def call_simple(_x), do: simple()
...> 
...>   @spec call_simple_constrain_to_nil(any()) :: nil
...>   def call_simple_constrain_to_nil(_x), do: simple()
...> 
...>   @spec call_simple_through_identity(any()) :: nil
...>   def call_simple_through_identity(_x), do: simple() |> identity()
...> end
{:module, TypedTest_Typed_MultiFunc0,
 <<70, 79, 82, 49, 0, 0, 7, 232, 66, 69, 65, 77, 69, 120, 68, 99, 0, 0, 1, 167,
   131, 104, 2, 100, 0, 14, 101, 108, 105, 120, 105, 114, 95, 100, 111, 99, 115,
   95, 118, 49, 108, 0, 0, 0, 4, 104, 2, ...>>,
 {:call_simple_through_identity, 1}}
iex> TypedTest_Typed_MultiFunc0.call_simple_through_identity(42)
nil
iex> TypedTest_Typed_MultiFunc0.call_simple(42)
nil
iex> TypedTest_Typed_MultiFunc0.identity(42)
42
iex> # call_simple_through_identity is properly typed with a return of nil as you notice
nil
11 Likes

Thanks for your work, it would be extremely great to elixir to have static type system!

2 Likes

Gradualizer is similar but it operates on the BEAM opcode level, so it both has more and less information in different ways. I can’t wait for a full release! :slight_smile:

2 Likes

So you gave up of your approach in favor of Gradualizer?

I really miss having a type system in Elixir. I know we have Dialyzer, but is not the same and is not straight forward to use.

2 Likes

I actually not miss a type system in Elixir. I absolutely hate having to work with Typescript at some work projects and wish I had Elixirs pattern matching. I guess it’s just personal preference. That said, if Elixir ever forced a type system on me, I would not use it anymore.

1 Like

Not really, I’d love to make a typed-elixir style system, I just don’t have time, at all… ^.^;

I entirely agree! A full and proper type system is not just for static type checking but for lots of other reasons as well, like code disambiguation, code generation based on types, etc… etc…

Typescript is… not really a great example of a type system, it’s awfully verbose. :wink:

2 Likes

I completely agree with you… It enables more confidence in how we code, less tests, and less code to check for stuff that should be work of the compiler or runtime.

I am looking forward to see how gleam by @lpil will develop :slight_smile:

4 Likes