MLElixir - attempting an ML-traditional syntax entirely within the Elixir AST

Also added the ability to call things from other calls, including all the usual currying and such, building on the prior example:

    def testering_func8 = testering_func1
    def testering_func9(a) = testering_func1 a
    def testering_func12(a, b) = testering_func1 a, b
    def testering_func20 = testering_func8 1, 2, 3
    def testering_func21 = testering_func9 1, 2, 3
    def testering_func22 = testering_func12 1, 2, 3
    def testering_func23 = testering_func9 1, _

Which can be used as:

    assert MLModuleTest_Specific.testering_func20() === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func21() === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func22() === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func23().(2, 3) === {1, 2, 3}

Which the generated Elixir is:

  def(testering_func20()) do
    testering_func8().(1, 2, 3)
  end
  def(testering_func21()) do
    testering_func9(1).(2, 3)
  end
  def(testering_func22()) do
    testering_func12(1, 2).(3)
  end
  def(testering_func23()) do
    fn ($var_0_22, $var__0_23) when is_integer($var__0_23) and is_integer($var_0_22) -> testering_func9(1).($var_0_22, $var__0_23) end
  end

More function tests and capabilities:

    def testering_func24 = testering_func9 1, _
    def testering_func25 = testering_func9 _, 2
    def testering_func26a(a, b, c, d, e) = {a, b, c, d, e}
    def testering_func26b(b) = testering_func26a 1, b
    def testering_func26c(c) = testering_func26b 2, c
    def testering_func26d(d) = testering_func26c 3, d
    def testering_func26(e) = testering_func26d 4, e
    def testering_func27(e) = testering_func26a _, _, _, _, e
    def testering_func28(e) = testering_func26b _, _, _, e
    def testering_func29(e) = testering_func26a _3, _2, _1, _0, e

Used from Elixir like:

    assert MLModuleTest_Specific.testering_func24().(2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func25().(1, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func26(5) === {1, 2, 3, 4, 5}
    assert MLModuleTest_Specific.testering_func27(5).(1, 2, 3, 4) === {1, 2, 3, 4, 5}
    assert MLModuleTest_Specific.testering_func28(5).(2, 3, 4) === {1, 2, 3, 4, 5}
    assert MLModuleTest_Specific.testering_func29(5).(4, 3, 2, 1) === {1, 2, 3, 4, 5}

Generates:

  def(testering_func24()) do
    fn ($var_0_22, $var_1_23) when is_integer($var_1_23) and is_integer($var_0_22) -> testering_func9(1).($var_0_22, $var_1_23) end
  end
  def(testering_func25()) do
    fn ($var_0_24, $var_0_25) when is_integer($var_0_25) and is_integer($var_0_24) -> testering_func9($var_0_24).(2, $var_0_25) end
  end
  def(testering_func26a(a, b, c, d, e)) do
    {a, b, c, d, e}
  end
  def(testering_func26b(b)) do
    fn $var_0_26, $var_1_27, $var_2_28 -> testering_func26a(1, b, $var_0_26, $var_1_27, $var_2_28) end
  end
  def(testering_func26c(c)) do
    fn $var_0_29, $var_1_30 -> testering_func26b(2).(c, $var_0_29, $var_1_30) end
  end
  def(testering_func26d(d)) do
    fn $var_0_31 -> testering_func26c(3).(d, $var_0_31) end
  end
  def(testering_func26(e)) do
    testering_func26d(4).(e)
  end
  def(testering_func27(e)) do
    fn $var_0_32, $var_1_33, $var_2_34, $var_3_35 -> testering_func26a($var_0_32, $var_1_33, $var_2_34, $var_3_35, e) end
  end
  def(testering_func28(e)) do
    fn $var_0_36, $var_0_37, $var_1_38 -> testering_func26b($var_0_36).($var_0_37, $var_1_38, e) end
  end
  def(testering_func29(e)) do
    fn $var_0_42, $var_1_41, $var_2_40, $var_3_39 -> testering_func26a($var_3_39, $var_2_40, $var_1_41, $var_0_42, e) end
  end

And most importantly, as always, if you pass in a wrong type it yells at you properly. ^.^

1 Like

Added the ability to call bindings, so you can pass functions around first-class now. Example

    def testering_func30(f) = f(2)
    def testering_func31(value, f) = f(value)

Used in Elixir itself like:

    assert MLModuleTest_Specific.testering_func30(fn x -> x + 1 end) === 3
    assert MLModuleTest_Specific.testering_func31(21, fn x -> x * 2 end) === 42

And it compiled as:

  def(testering_func30(f) when is_function(f, 1)) do
    f.(2)
  end
  def(testering_func31(value, f) when is_function(f, 1)) do
    f.(value)
  end

I may have also added support for operator overloading (only the elixir set since I am working within Elixir’s AST after all), so this works:

    def value |> fun = fun(value)
    # let value |> fun = fun(value) # Both work as always
    def testering_op_pipe0 = 42 |> testering_tuple5
    def identity(i) = i
    def testering_op_pipe1(i) =
      42
      |> identity
      |> testering_func1(1, _, i)

Which can be used from Elixir like:

      import Kernel, except: [|>: 2]
      import MLModuleTest_Specific, only: [|>: 2]
      assert (21 |> (&(&1*2))) === 42
      assert MLModuleTest_Specific.testering_op_pipe0() === {42}
      assert MLModuleTest_Specific.testering_op_pipe1(3) === {1, 42, 3}

Which generates this code:

  def(value |> fun when is_function(fun, 1)) do
    fun.(value)
  end
  def(testering_op_pipe0()) do
    42 |> fn $var_44 -> (fn $var_0_43 when is_integer($var_0_43) -> testering_tuple5($var_0_43) end).($var_44) end
  end
  def(identity(i)) do
    i
  end
  def(testering_op_pipe1(i) when is_integer(i)) do
    42 |> fn $var_46 -> (fn $var_0_45 -> identity($var_0_45) end).($var_46) end |> fn $var_48 -> (fn $var_0_47 when is_integer($var_0_47) -> testering_func1(1, $var_0_47, i) end).($var_48) end
  end

Enhanced bindings to become full matches, thus you can now destructure things, like this:

    def testering_binding0(i) = i
    def testering_binding1(i | integer) = i
    def testering_binding2(42) = 42
    def testering_binding3(42 | integer) = 42
    def testering_binding4(42 = i) = i
    def testering_binding5(i = 42) = i
    def testering_binding6({i}) = i
    def testering_binding7({42}) = 42
    def testering_binding8({i | integer}) = i
    def testering_binding9({i | integer} | {integer}) = i
    def testering_binding10({i} | {integer}) = i
    def testering_binding11({i}=t) = {i, t}
    def testering_binding12(%{a: i | integer}) = i
    def testering_binding13(%{a: i | integer}=r) = %{b: r}

Which can be used from Elixir itself like this:

    assert MLModuleTest_Specific.testering_binding0(42) == 42
    assert MLModuleTest_Specific.testering_binding1(42) == 42
    assert MLModuleTest_Specific.testering_binding2(42) == 42
    assert MLModuleTest_Specific.testering_binding3(42) == 42
    assert MLModuleTest_Specific.testering_binding4(42) == 42
    assert MLModuleTest_Specific.testering_binding5(42) == 42
    assert MLModuleTest_Specific.testering_binding6({42}) == 42
    assert MLModuleTest_Specific.testering_binding7({42}) == 42
    assert MLModuleTest_Specific.testering_binding8({42}) == 42
    assert MLModuleTest_Specific.testering_binding9({42}) == 42
    assert MLModuleTest_Specific.testering_binding10({42}) == 42
    assert MLModuleTest_Specific.testering_binding11({42}) == {42, {42}}
    assert MLModuleTest_Specific.testering_binding12(%{a: 42}) == 42
    assert MLModuleTest_Specific.testering_binding13(%{a: 42}) == %{b: %{a: 42}}

And is compiled to this:

  def(testering_binding0(i)) do
    i
  end
  def(testering_binding1(i) when is_integer(i)) do
    i
  end
  def(testering_binding2(42)) do
    42
  end
  def(testering_binding3(42)) do
    42
  end
  def(testering_binding4(42 = i) when is_integer(i)) do
    i
  end
  def(testering_binding5(i = 42) when is_integer(i)) do
    i
  end
  def(testering_binding6({i})) do
    i
  end
  def(testering_binding7({42})) do
    42
  end
  def(testering_binding8({i}) when is_integer(i)) do
    i
  end
  def(testering_binding9({i}) when is_integer(i)) do
    i
  end
  def(testering_binding10({i}) when is_integer(i)) do
    i
  end
  def(testering_binding11({i} = t) when tuple_size(t) === 1 and is_tuple(t)) do
    {i, t}
  end
  def(testering_binding12(%{a: i}) when is_integer(i)) do
    i
  end
  def(testering_binding13(%{a: i} = r) when is_map(r) and is_integer(i)) do
    %{b: r}
  end

Sooo, I guess now real things could start being written… ^.^

Fixed typing bugs, I.E. types fully go ‘up’ to function arguments now too, was shown by and is fixed now in code like:

defmlmodule Testering do
  def identity_typed(i|integer) = i
  def pipe(value, f) = f(value)
  def tester(i) = pipe(i, identity_typed)
end

Which generates code like:

defmodule(Testering) do
  import(Kernel, only: [def: 2, is_integer: 1, is_function: 2])
  def(identity_typed(i) when is_integer(i)) do
    i
  end
  def(pipe(value, f) when is_function(f, 1)) do
    f.(value)
  end
  def(tester(i) when is_integer(i)) do
    pipe(i, fn $var_1 when is_integer($var_1) -> (fn $var_0_0 when is_integer($var_0_0) -> identity_typed($var_0_0) end).($var_1) end)
  end
end

I think I need to collapse some wrappers down when they are identical… >.>
Only an optimization though (and since they are internally private the EVM/BEAM should make them vanish anyway, so I don’t really care right now).

Either way, things like this are now fully type safe and properly error out if you try to do something like this:

defmlmodule Testering do
  def identity_typed(i|integer) = i
  def value |> fun = fun(value)
  def tester1(i | float) = i |> identity_typed
  def tester2() = 6.28 |> identity_typed
end

Both tester1/1 and tester2/0 will error at compile-time with something like:

No Type Resolutions between float and integer

2 Likes

This is very cool. Don’t you want to go full string to get rid of elixir’s AST? A strongly typed language with interfaces to elixir and using mix would be pretty cool.

Or even a strongly typed lisp with HM types

1 Like

I actually have an ElixirML project (with *.eml files) for that, and yes you can embed strings in a macro by calling it directly as well. This project (the whole ‘Typed Elixir’ project) is just for screwing around with and seeing how far I can push Elixir Macro’s. :slight_smile:

eml is not ready for consumption and it is mostly a mish-mash of code (as is MLElixir really too, the git repo is the code that ‘works together’, it is not the main bulk of code, which I keep on bitbucket and gitlab (backups, gitlab is soooo sloooow)). If I decide to take the code, clean it up a bit and make it in to a proper system then I’d put it on github, but right now it is ‘not’ pleasant to look at the code for it (though it works’ish). ^.^

def doStuff(c) do
  eml [c: c], """
    let a = 21 in
    let b = a * c in
    b + SomeEmlModule.blah 42
  """
end

I have thought of making an lfe style project but to integrate with elixir (integration with elixir macro’s and all) with Types (I so love types, I could even handle the weird Elixir syntax fine if Elixir had Types) too, just never got around to it yet. I’ve been moving houses the past month+ so my home programming time has vanished currently (should resume in the next week or two, whooo), but that has been an idea as well. :slight_smile:

/me implements too many languages, almost made as many in Elixir as has made in C++, even made a BEAM-style distributed async setup for C++ almost a decade ago…

I tend to lean to languages that are both typed as well as are as homoiconic as I can really make them for their problem domain. :slight_smile:

2 Likes

If you’re taking user feedback, I’d prefer LFE + types + elixir interop :slight_smile:

1 Like

Lol, I always do, and as there is interest in that then my next playground project might be just that. ^.^

What do you mean by “elixir interop”. There is no problem calling elixir<->lfe of course and we even have binary utf-8 encoded string as well. #“detta är en binär sträng”

The latest version of LFE has a type and spec syntax which allows you to write Erlang compatible types. No checking yet. I will fix it to make dialyzer usable for LFE. Had it working a while back but that was a quick hack, which did actually work.

LFE doesn’t compile to erlang AST but straight to Core erlang.

1 Like

It’s mostly types actually :slight_smile: I’d like a statically typed lisp. I’ve never really looked at LFE and only know it is dynamically typed from one of your talks. I didn’t mean to imply that LFE didn’t support calling Elixir and vice versa. I just wanted to try a statically typed lisp running on the BEAM, whether it is LFE or something else.

But using dialyzer means you’re limited to success typing instead of something like the Hindley–Milner type system (or equivalent), right?

1 Like

Also, just checked the official LFE website:

Utterly Terrifying

  • Fault-tolerant
  • Massively scalable
  • Concurrent
  • Soft real-time
  • Open. Telecom. Platform.

LOOOOOOOOOOOOOOL

2 Likes

Utterly Terrifying

Hehe, it really does say that, I love it! ^.^

Maybe you could work with @rvirding and the LFE team to add optional HM types to LFE? I don’t know if that’s possible without changing something fundamental about the language

1 Like

I’m like 95% certain it would require certain it would require changing the language in a backwards incompatible way even without looking at its compiler yet. ^.^

You could always implement it ‘inside’ the language via lisp’y macro’s though.

What about a strict version of dialyzer that works on Elixir AST, uses HM type inference and reject all code it CAN’T prove correct? With the possibility for “dirty” type coercions, of course, otherwise you’d get the problem of typing receive. Is this possible? Doesn’t seem too hard…

I’m by no means a fan of Elixir syntax, but at least being able to reuse Mix an the existing macros is pretty cool, so something that works on vanilla elixir files is pretty cool.

2 Likes

I already have a project that started as something like that along with code specialization based on types (which is the real nice thing about typing). It is always just an issue of ‘time’ for me. ^.^;

Including memory safety and “let-it-crash” philosophy? :hushed:

As long as you don’t use mutation then yes memory safety (and if linear types get added to OCaml then even those would be safe), but that could be enforced be a non-mutable message type too, and let-it-crash easily so as each micro-thread can be wrapped in an exception handler (which is consequently how effects work as well, so it’s the same code).

1 Like

That sounds like source to source converters count as superfluous to me now.
At least when it comes from an ML language to Elixir.

I am still concerned, that F-Sharp provides the saner syntax, even when I see it currently not so drastically anymore as before. It provides an ML compability mode, with which I can produce OCaml code, so far as I understand. The syntax changes of ReasonML do I consider still as helpful, so long as all the changes regarding to make it more JS like get reversed…