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

Little more work, a large revamp of how the code works (more shared code with Typed Elixir whoo!). Module types are a thing now, and consequently so are accessing remote type, and thus also made a defmlmodule call, all of this works and dies if the types do not match as expected:



import MLElixir
defmlmodule MLModuleTest do

  type type_declaration

  type type_definition = integer

  def test_int_untyped = 42
  def test_int_typed | integer = 42
  def test_float_untyped = 6.28
  def test_float_typed | float = 6.28
  def test_defined | type_definition = 42
  def identity_untyped(x) = x
  def identity_typed(x | +id_type) | +id_type = x
  def identity_int(x | integer, _f | float) | integer = x
  def identity_float(_x | integer, f | float) | float = f
  def test_block0 do 42 end
  def test_blockN0() do 42 end
  def test_blockT0() | integer do 42 end
  def test_blockN1(x) do x end
  def test_blockT1(x | +id_type) | +id_type do x end

end


defmlmodule MLModuleTest_Generalized do
  type t

  def blah(t | t) | t, do: t
end


defmlmodule MLModuleTest_Specific do
  type t = MLModuleTest.type_definition
  type Specific = MLModuleTest_Generalized.(t: float)

  def testering0 | t = 42
end

Parameterized types and overloadable types are done via .(blah) syntax. So to refine a type (say a ‘Dict’ module has a type ‘t’ that is generic, you can refine it to a specific type that fits within generic (anything) and use the module with that kind of type) you specify it with the standard proplist style with the key as the id, so in the examples above that is the type specific = MLModuleTest_Generalized.(t: float) call, it refines the t type on MLModuleTest_Generalized to make a more refined type. You can then call on Specific to access it but with the refined type, however I’m not quite happy with this syntax (not able to ‘pass’ a module around), so going to change it sometime when I get time so that modules can be packed as a value like in OCaml, pretty easy to do on the BEAM. ^.^

However, the same syntax will also be used to apply types as well, so given a type like:

type result(ok, error) = enum # still unsure what to call this type, variant is long...
| ok = ok
| error = error

You can refine it like result.(String.t, nil) or like result.(ok: String.t, error: nil) for explicitness. ^.^

The . (dot) calling convention is used for applying types, the normal (or left out) parenthesis is for function application. Still up in the air of course.

EDIT: Using ‘def’ because syntax coloring, ‘let’ is supported too, and it supports 'let’ish style and elixir do/block style.

Also, a type name like integer refers to a built-in or pre-named type, a type name prefixed with + like +id_type is a named generic.

2 Likes

Or at least MLixir (though ExML is genius).

2 Likes

I cannot help but read that as “Em Licks Her”… >.>

Lol, but the name can still easily be changed if this thing is worth releasing someday. ^.^

1 Like

Got type application in, it can type down to any type’s type, I should probably get more types in… ^.^

  defmlmodule MLModuleTest_Specific do
    type t = MLModuleTest.type_definition
    type Specific = MLModuleTest_Generalized.(t: float)
    type st = Specific.t
    type a(t) = t
    type b(c) = c
    type ra = a(integer)
    type rb = b(integer)

    def testering0 | t = 42
    def testering1 | st = 6.28
    def testering2 | ra = 42
    def testering3 | rb = 42
    def testering4 | a(integer) = 42
    def testering5 | a(float) = 6.28
  end

I was going back and forth on syntax, between these options:

type a(t) = t
type a.(t) = t
type a[t] = t
type a t = t

I was really torn between the last one, the second to last, and the first, but due to limitations in Elixir’s syntax the last version could potentially cause issues when I want to start refining the arguments to a limited set and the other versions have no such limitation. Between the first and third, the third seemed nice from a type perspective, but it means I cannot use it for some other type purposes later that I am wanting. So that left me with the a(b, c) format for the fewest issues in the Elixir syntax, so I went with it. :slight_smile:

1 Like

Got first version of a variant/union/enum/adt/etc… type in now too, it is planned to be expanded substantially:

  defmlmodule MLModuleTest_Specific do
    type t = MLModuleTest.type_definition
    type Specific = MLModuleTest_Generalized.(t: float)
    type st = Specific.t
    type a(t) = t
    type b(c) = c
    type ra = a(integer)
    type rb = b(integer)

    type testering_enum
    | none
    | one
    | integer integer
    | two
    | float float
    | float2 float

    def testering0 | t = 42
    def testering1 | st = 6.28
    def testering2 | ra = 42
    def testering3 | rb = 42
    def testering4 | a(integer) = 42
    def testering5 | a(float) = 6.28
    def testering6 | testering_enum = none()
    def testering7 | testering_enum = one
    def testering8 | testering_enum = two
    def testering9 | testering_enum = integer # Curried!
    def testering10 | testering_enum = integer 42
    def testering11(x) | testering_enum = integer x
  end

Calling MLModuleTest_Specific.testering11(42) for example will return {:integer, 42} as right now value-less heads (like none) are encoded as the atom of their name (so :none) and multi-value ones (like integer integer) are encoded as a tuple of the same length as the args +1 where the first element is the atom of the name, kind of like erlang records. I have syntax ready to handle a few other various ones such as maps (they will act like elixir structs) and possible a keyword list but unsure if I want that one…

Oh, and I got guards generating now, so calling MLModuleTest_Specific.testering11(6.28) throws this error:

     ** (FunctionClauseError) no function clause matching in MLElixirTest.MLModuleTest_Specific.testering11/1
     stacktrace:
       test/ml_elixir_test.exs:69: MLElixirTest.MLModuleTest_Specific.testering11(6.28)
       test/ml_elixir_test.exs:108: (test)

I’m planning to generate guards and matchers and other conditional checks to ensure the types match what the function is expecting, I might make an option to not generate those either but honestly I’m not wanting to lean that way that way I can know for sure guards are available for some better matchers later.

Oh, and also yes, auto-currying is started, I added it to the adt heads because I hate making manual wrappers for those, as you can see in testering9/1, if you call that function it returns another function, which you can then call as expected, thus calling MLModuleTest_Specific.testering9().(42) from elixir returns {:integer, 42}

I’m building this so functions are not auto-curried, so if you want to call a function then its arguments must be complete, that way you can control which erlang/elixir/whatever function is called by arity, but to compensate I’m having auto-currying happen at the call site (which is the erlang way anyway), as you can see with testering9/1, that will happen with all calls, partially applied arguments will be auto-wrapped in an anonymous function. :slight_smile:

1 Like

Got a few minutes available today so I got records in, usage:


    type record0 = %{} # Empty record
    type record1 = %{
      x: integer,
      y: float,
    }
    type record2(t) = %{t: t}
    type record_ex_0 = %{_: record0, z: integer}
    type record_ex_1 = %{_: record1, z: integer}
    type record_ex_2(t) = %{_: record2(t), z: integer}
    type record_ex_2_float = %{_: record2(float), z: integer}

    def testering_record0 | record0 = %{}
    def testering_record1 | record1 = %{x: 42, y: 6.28}
    def testering_record2(i) | record1 = %{x: i, y: 6.28}
    def testering_record3(t | !t) | record2(!t) = %{t: t}
    def testering_record4 | record_ex_0 = %{z: 42}
    def testering_record5 | record_ex_1 = %{x: 42, y: 6.28, z: 42}
    def testering_record6 | record_ex_2(integer) = %{t: 42, z: 42}
    def testering_record7(t | !t) | record_ex_2(!t) = %{t: t, z: 42}
    def testering_record8 | record_ex_2_float = %{t: 6.28, z: 42}

And the tests pass:

    assert MLModuleTest_Specific.testering_record0() === %{}
    assert MLModuleTest_Specific.testering_record1() === %{x: 42, y: 6.28}
    assert MLModuleTest_Specific.testering_record2(42) === %{x: 42, y: 6.28}
    assert MLModuleTest_Specific.testering_record3(42) === %{t: 42}
    assert MLModuleTest_Specific.testering_record4() === %{z: 42}
    assert MLModuleTest_Specific.testering_record5() === %{x: 42, y: 6.28, z: 42}
    assert MLModuleTest_Specific.testering_record6() === %{t: 42, z: 42}
    assert MLModuleTest_Specific.testering_record7(42) === %{t: 42, z: 42}
    assert MLModuleTest_Specific.testering_record8() === %{t: 6.28, z: 42}

And the usual typing failures fail as expected, and as you can see a record maps to an elixir map with atom() keys and any value (so kind of like elixir structs, except typed).

As you can see it supports static typing the fields as well as extended the types as well. I have row typing/polymorphism planned as well with a syntax that works.

2 Likes

I do have one question, I’m using the underscore atom as the extension syntax right now due to similarity with erlang records, but there is the option of using elixir’s map update syntax like %{record0 | z: integer} however I want to use that in expressions to update individual elements and so that adds a discrepency between type and expressions syntax that I do not like. I did have another idea in mind, which allows very fine grained control of both adding and removing record elements, however it would mean not allowing the plus and minus atoms either, but it would work like this in the full full version of it:

%{
  +: record0,
  +: record1,
  -: x,
  +: record2,
  -: z
}
  • At the record0 line it adds all record0’s records to this record, so its basically record0 at this point.
  • At the record1 line it adds all record1’s fields to this record, if any duplicates happen with record0 then it is a compile error, but should I make it not error if the fields are identically typed? I’m unsure yet, thoughts?
  • At the x line it removed the current field with the name of x from this record type, compile error if it does not exist.
  • At the record2 line it then adds all fields in record2 to this record, ditto as before.
  • At the z line it removed the current field with the name of z from this record type, compile error again if it does not exist.

It is more powerful, and the EVM/BEAM supports it fine, but should I do it?

Went ahead and made that change, can add/remove types now:

    type record0 = %{} # Empty record
    type record1 = %{
      x: integer,
      y: float,
    }
    type record2(t) = %{t: t}
    type record_ex_0 = %{+: record0, z: integer}
    type record_ex_1 = %{+: record1, z: integer}
    type record_ex_2(t) = %{+: record2(t), z: integer}
    type record_ex_2_float = %{+: record2(float), z: integer}
    type record_rem_0 = %{+: record1, -: x}

And of course trying to do this:

def testering_record10 | record_rem_0 = %{x: 42, y: 6.28}

Gives you this:

{:NO_TYPE_RESOLUTION, :RECORD_LENGTH_DOES_NOT_MATCH, [:x, :y], [:y]}

And trying to do the wrong one:

def testering_record10 | record_rem_0 = %{x: 42}

Gives you:

{:NO_TYPE_RESOLUTION, :RECORD_LABEL_MISMATCH, :x, :EXPECTED, :y}

And the wrong type returns the usual type error of cannot cast, say, integer to float. :slight_smile:

EDIT: And of course no duplicate keys allowed still, even from multiple records:

type record_ex_1 = %{+: record1, +: record1, z: integer}

Results in:

{:RECORD_DUPLICATE_LABELS, [:x, :y]}

Also added an FFI interface to type and call to arbitrary erlang/elixir calls, like here is integer addition:

    external addi(integer, integer) | integer = Kernel.+

Calling it is as expected:

    def testering_ffi_addi_0 = addi(1, 2)
    def testering_ffi_addi_1(i) = addi(1, i)
    def testering_ffi_addi_2(a, b) = addi(a, b)

In addition if you call it from non-typed (normal elixir) code, you can call the external directly, so just this from normal elixir:

assert MLModuleTest_Specific.addi(1, 2) === 3

And that works, and is properly typed, so if you try passing, say, 2.2 instead of 2 there will be no head match as normal, so you can have some of those guarantees as normal but for elixir too.

EDIT: I have alternative names for a lot of things too, so instead of external you can use defexternal, and instead of def you can use let, and for the def/let you can use = or you can use , do:, whichever all works whether you want it more ML’y or more Elixir’y. :slight_smile:

2 Likes

Barely started it, but wanted to play for a second, however this:

    js =
      defmlmodule :testing_js, output_format: :js do
        type t = MLModuleTest.type_definition
        type Specific = MLModuleTest_Generalized.(t: float)
        type st = Specific.t
        type a(t) = t
        type b(c) = c
        type ra = a(integer)
        type rb = b(integer)

        type testering_enum
        | none
        | one
        | integer integer
        | two
        | float float
        | float2 float

        def testering0 | t = 42
        def testering1 | st = 6.28
        def testering2 | ra = 42
        def testering3 | rb = 42
        def testering4 | a(integer) = 42
        def testering5 | a(float) = 6.28
        def testering6 | testering_enum = none() # Just testing that it works with 0-args too, elixir ast oddness reasons
        def testering7 | testering_enum = one
        def testering8 | testering_enum = two
        def testering9 | testering_enum = integer # Curried!
        def testering9x(x) | testering_enum = integer x # Not-Curried!
        def testering10 | testering_enum = integer 42
        def testering11(x) | testering_enum = integer x
      end

Currently translates into this for the string in js:

// Module Name:  testing_js
// Version: 0.0.1

function testering0() {
        return 42;
}
function testering1() {
        return 6.28;
}
function testering2() {
        return 42;
}
function testering3() {
        return 42;
}
function testering4() {
        return 42;
}
function testering5() {
        return 6.28;
}
function testering6() {
        return 'none';
}
function testering7() {
        return 'one';
}
function testering8() {
        return 'two';
}
function testering9() {
        return function(__ANON_ARG__){return['integer', __ANON_ARG__];};
}
function testering9x(x) {
        return x;
}
function testering10() {
        return 42;
}
function testering11(x) {
        return x;
}

Which is just the start of the tests. It is nice to see alternate output though. ^.^

1 Like

Finished up the javascript conversion, it works, unsure about the final output, but eh…

Given this:

  test "MLElixir Javascript output" do
    js =
      defmlmodule :testing_js, output_format: :js do
        type t = MLModuleTest.type_definition
        type Specific = MLModuleTest_Generalized.(t: float)
        type st = Specific.t
        type a(t) = t
        type b(c) = c
        type ra = a(integer)
        type rb = b(integer)

        type testering_enum
        | none
        | one
        | integer integer
        | two
        | float float
        | float2 float

        def testering0 | t = 42
        def testering1 | st = 6.28
        def testering2 | ra = 42
        def testering3 | rb = 42
        def testering4 | a(integer) = 42
        def testering5 | a(float) = 6.28
        def testering6 | testering_enum = none() # Just testing that it works with 0-args too, elixir ast oddness reasons
        def testering7 | testering_enum = one
        def testering8 | testering_enum = two
        def testering9 | testering_enum = integer # Curried!
        def testering9x(x) | testering_enum = integer x # Not-Curried!
        def testering10 | testering_enum = integer 42
        def testering11(x) | testering_enum = integer x

        type record0 = %{}
        type record1 = %{
          x: integer,
          y: float,
        }
        type record2(t) = %{t: t}
        type record_ex_0 = %{+: record0, z: integer}
        type record_ex_1 = %{+: record1, +: record0, z: integer}
        type record_ex_2(t) = %{+: record2(t), z: integer}
        type record_ex_2_float = %{+: record2(float), z: integer}
        type record_rem_0 = %{+: record1, -: x}

        def testering_record0 | record0 = %{}
        def testering_record1 | record1 = %{x: 42, y: 6.28}
        def testering_record2(i) | record1 = %{x: i, y: 6.28}
        def testering_record3(t | !t) | record2(!t) = %{t: t}
        def testering_record4 | record_ex_0 = %{z: 42}
        def testering_record5 | record_ex_1 = %{x: 42, y: 6.28, z: 42}
        def testering_record6 | record_ex_2(integer) = %{t: 42, z: 42}
        def testering_record7(t | !t) | record_ex_2(!t) = %{t: t, z: 42}
        def testering_record8 | record_ex_2_float = %{t: 6.28, z: 42}
        def testering_record10 | record_rem_0 = %{y: 6.28}

        # FFI
        external addi(integer, integer) | integer = Kernel.add
        def testering_ffi_addi_0 = addi(1, 2)
        def testering_ffi_addi_1(i) = addi(1, i)
        def testering_ffi_addi_2(a, b) = addi(a, b)
      end

    IO.puts(js)
  end

Outputs this:

// Module Name:  testing_js
// Version: 0.0.1

function testering0() {
        return 42;
}
function testering1() {
        return 6.28;
}
function testering2() {
        return 42;
}
function testering3() {
        return 42;
}
function testering4() {
        return 42;
}
function testering5() {
        return 6.28;
}
function testering6() {
        return 'none';
}
function testering7() {
        return 'one';
}
function testering8() {
        return 'two';
}
function testering9() {
        return function(__ANON_ARG__){return['integer', __ANON_ARG__];};
}
function testering9x(x) {
        return x;
}
function testering10() {
        return 42;
}
function testering11(x) {
        return x;
}
function testering_record0() {
        return {};
}
function testering_record1() {
        return {"x": 42, "y": 6.28};
}
function testering_record2(i) {
        return {"x": i, "y": 6.28};
}
function testering_record3(t) {
        return {"t": t};
}
function testering_record4() {
        return {"z": 42};
}
function testering_record5() {
        return {"x": 42, "y": 6.28, "z": 42};
}
function testering_record6() {
        return {"t": 42, "z": 42};
}
function testering_record7(t) {
        return {"t": t, "z": 42};
}
function testering_record8() {
        return {"t": 6.28, "z": 42};
}
function testering_record10() {
        return {"y": 6.28};
}
function addi(add__var__0, add__var__1) {
        return require("Kernel").add(add__var__0, add__var__1);
}
function testering_ffi_addi_0() {
        return require("Kernel").add(1, 2);
}
function testering_ffi_addi_1(i) {
        return require("Kernel").add(1, i);
}
function testering_ffi_addi_2(a, b) {
        return require("Kernel").add(a, b);
}
1 Like

And yes, changing the ffi to be multi-pathed works as expected:

        external addi(integer, integer) | integer = Kernel.Blah.add

Changes the relevant parts in the javascript to:

function addi(add__var__0, add__var__1) {
        return require("Kernel").Blah.add(add__var__0, add__var__1);
}
function testering_ffi_addi_0() {
        return require("Kernel").Blah.add(1, 2);
}
function testering_ffi_addi_1(i) {
        return require("Kernel").Blah.add(1, i);
}
function testering_ffi_addi_2(a, b) {
        return require("Kernel").Blah.add(a, b);
}
1 Like

Very interesting project @OvermindDL1. Thanks for sharing it with us. Kind of makes me want to learn ML :slight_smile:

3 Likes

I’d recommend OCaml, even if you do not end up using it, the ideas and ways of thinking it imparts on you will help you everywhere. :slight_smile:

I’m trying to emulate OCaml’s syntax as much as I can, but cannot get very close to it with how Eixir’s AST is parsed, I’d have to drop down to a string if I wanted it proper. ^.^

1 Like

I started looking at Haskell a couple weeks ago. Just to see if it could help me with my Elixir. Got a little lost as I read along. Had a hard time remember the syntax of early chapters since I did not do very many exercises. At 50+ years old, my memory is not like it used to be.

I’ll take a quick peek at OCaml. Thanks for the recommendation.

1 Like

OCaml and Haskell take divergent paths from H-M Type Systems.

OCaml came from SML, which came from ML, which is short for Module Language. OCaml/SML/ML do the higher H-M typing via the module system.

Haskell, well I’m not sure where it came from, but it went for typeclasses and 80 other things to try to emulate what modules can do.

Haskell was designed at a university, to be a research language, and it will never really ever be ‘finished’ and is constantly changing. Consequently it is hard to get a lot done in it because you have to import a half dozen compiler extensions (Language Blah stuff at the top of files) just to do things that are fairly trivial with modules.

SML is basically ML with a ‘Standard’ defined set to unify on. OCaml expanded on SML by adding an object system (that honestly no one really uses since modules can do it all anyway) as well as making modules entirely first-class, meaning that you can now do higher typing on higher types, which is what makes OCaml the one that can truly do anything Haskell can do and a lot more that Haskell cannot do.

However, OCaml’s syntax is very uniform, once you learn modules and functors then you can do it all, you can emulate Haskell’s typeclasses, emulate Haskell’s HKT’s, etc… etc… And due to how the modules are designed in terms of non-global completeness they are blazing fast to compile, and this is why horrendously huge and complex OCaml programs still take less than a minute to compile, where comparatively simple Haskell programs can take minutes, and getting up to 30-60 minute compiles in Haskell is common with fairly simple programs, in addition OCaml’s optimizing compiler also generates code significantly faster than Haskell, making it within an order of speed of optimized C++ on average. The only thing Haskell does ‘better’ right now is muti-threadedness, but OCaml has a finalizing a concurrent PR for OCaml to add far far better functionality (you could even emulate the BEAM with it’s upcoming setup).

However, unlike Haskell that iterates fast and breaks often, OCaml was designed By businesses For businesses, so every choice in it is designed to solve a problem and solve it well and permanently, so it is slow moving, but when something gets accepted into mainline then you know it is well tested and hardened. OCaml was designed to get work done, and you can even sacrifice purity and put in a little magic (literally) to Get Stuff Done, just like how Erlang was designed.

But yes, learning OCaml is significantly easier than Haskell due to a more simple language with few constructs to learn, where Haskell has literal thousands of constructs depending on the extensions you enable, and even base Haskell has significantly more constructs than OCaml and still cannot do what OCaml does.

The OCaml compiler is highly pluggable as well, endlessly extendable, and there are other backends for it as well, including a Javascript back-end (a couple actually), and the OCaml compiler has been compiled with that backend to generate a javascript version of the compiler, so if you want to see the compiler’s utter speed as-you-type even while running in javascript, then you can type OCaml here and see the javascript output and the executed results as-you-type: https://bloomberg.github.io/bucklescript/js-demo/index.html

3 Likes

Actually he’s made an intro Setting up Bucklescript with Phoenix which may be one way to get started.

I’ve been chasing Haskell off and on since 1998; “The Haskell School of Expression”, “Haskell The Craft of Functional Programming 2e”, “Programming Haskell”, “Real World Haskell” - so far Haskell Programming from first Principles makes the most sense without having to take a course in category theory first.

What resources can you recommend for non-hyperglots? (I may have missed that somewhere - OK you just beat me to it).
Not everybody attacks programming languages the same way you do :icon_biggrin: .

Lol, the official docs and tutorial site are great starting places. :slight_smile:

Also, I apparently responded to the post like a second before you did… ^.^;

1 Like

Some more today, got tuples in, and made a tupleizable version of ADT’s, used like:

    | tuple_enum0{}
    | tuple_enum1{integer}
    | tuple_enum2{integer, float}

If used like:

    def testering20 = tuple_enum0
    def testering21 = tuple_enum1 42
    def testering22 = tuple_enum2
    def testering23 = tuple_enum2 42
    def testering24(f) = tuple_enum2 42, f
    def testering25 = tuple_enum2 42, 6.28

All results in this:

    assert MLModuleTest_Specific.testering20 === {:tuple_enum0}
    assert MLModuleTest_Specific.testering21 === {:tuple_enum1, 42}
    assert MLModuleTest_Specific.testering22.(42, 6.28) === {:tuple_enum2, 42, 6.28}
    assert MLModuleTest_Specific.testering23.(6.28) === {:tuple_enum2, 42, 6.28}
    assert MLModuleTest_Specific.testering24(6.28) === {:tuple_enum2, 42, 6.28}
    assert MLModuleTest_Specific.testering25 === {:tuple_enum2, 42, 6.28}

If you really do want a tuple value, then just double it up:

    | tuple_recurception{{integer, float}}

Used like:

    def testering27 = tuple_recurception {42, 6.28}

Which returns:

    assert MLModuleTest_Specific.testering27 === {:tuple_recurception, {42, 6.28}}

In addition, normal tuple stuff:

    type tuple0 = {}
    type tuple1 = {integer}
    type tuple2 = {integer, float}
    type tuple3 = {integer, {float, integer}}

Used like:

    def testering_tuple0 | tuple0 = {}
    def testering_tuple1(t) | tuple0 = t
    def testering_tuple2 = {}
    def testering_tuple3 = {42}
    def testering_tuple4 | tuple1 = {42}
    def testering_tuple5(i) | tuple1 = {i}
    def testering_tuple6(t) | tuple1 = t
    def testering_tuple7 | tuple1 = testering_tuple3
    def testering_tuple8 = {42, 6.28}
    def testering_tuple9(i|!t) = {i, 6.28}
    def testering_tuple10(i) | tuple2 = testering_tuple9 i
    def testering_tuple11 | tuple2 = testering_tuple9 41
    def testering_tuple12 | tuple3 = {42, {6.28, 42}}

Which returns:

    assert MLModuleTest_Specific.testering_tuple0() === {}
    assert MLModuleTest_Specific.testering_tuple1({}) === {}
    assert MLModuleTest_Specific.testering_tuple2() === {}
    assert MLModuleTest_Specific.testering_tuple3() === {42}
    assert MLModuleTest_Specific.testering_tuple4() === {42}
    assert MLModuleTest_Specific.testering_tuple5(42) === {42}
    assert MLModuleTest_Specific.testering_tuple6({42}) === {42}
    assert MLModuleTest_Specific.testering_tuple7() === {42}
    assert MLModuleTest_Specific.testering_tuple8() === {42, 6.28}
    assert MLModuleTest_Specific.testering_tuple9(42) === {42, 6.28}
    assert MLModuleTest_Specific.testering_tuple10(42) === {42, 6.28}
    assert MLModuleTest_Specific.testering_tuple11() === {42, 6.28}
    assert MLModuleTest_Specific.testering_tuple12() === {42, {6.28, 42}}

Fairly type safe as expected, you cannot even pass a float to, say, MLModuleTest_Specific.testering_tuple5(6.28) as you will get a resolution error at compile-time.

Also added in support for calling in to records:

    type record_emb_0 = %{a: %{b: %{c: integer}}}
    def testering_record11(r | record_emb_0) = r.a.b.c

Properly checked and typesafe and so forth as usual. I do not have auto-detection for a generic record type at this time, and honestly I’m unsure if I want to as it would make . ambiguous between different types, so that may require typing like the above unless I want to do a special syntax or something… Thoughts?

Also, for some reason I was not handling 0-arg function calls yet, fixed that so they work now too… >.>

Decided to add auto-currying as well. Unlike other auto-currying languages on the beam (*cough*alpaca**cough*elmchamy*cough*) that auto-curry at the definition site, thus using up every single arity of a function, I’m auto-currying at the call site, which is something the BEAM is well optimized for anyway, thus given these definitions:

    def testering_func0 = 42
    def testering_func1(a, b, c) | {integer, integer, integer} = {a, b, c}
    def testering_func2(a | integer, b | integer, c | integer) = {a, b, c}
    def testering_func3(a | integer, b | integer, c | integer) | {integer, integer, integer} = {a, b, c}
    def testering_func4 = testering_func0
    def testering_func5 = testering_func1 1, 2, 3
    def testering_func6(a, b, c) = testering_func1 a, b, c
    def testering_func7(a) = testering_func1 1, a, 3
    def testering_func8 = testering_func1
    def testering_func9(a) = testering_func1 a
    def testering_func10(a) = testering_func1 1, a
    def testering_func11(a) = testering_func1 a, 2
    def testering_func12(a, b) = testering_func1 a, b
    def testering_func13(a) = testering_func1 a, _, _
    def testering_func14(a) = testering_func1 _, a, _
    def testering_func15(a) = testering_func1 _, _, a
    def testering_func16(a) = testering_func1 _, a
    def testering_func17(a) = testering_func1 a, _0, _1
    def testering_func18(a) = testering_func1 a, _1, _0
    def testering_func19(a) = testering_func1 a, _1

Which can be used like:

    assert MLModuleTest_Specific.testering_func0() === 42
    assert MLModuleTest_Specific.testering_func1(1, 2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func2(1, 2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func3(1, 2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func4() === 42
    assert MLModuleTest_Specific.testering_func5() === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func6(1, 2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func7(2) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func8().(1, 2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func9(1).(2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func10(2).(3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func11(1).(3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func12(1, 2).(3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func13(1).(2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func14(2).(1, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func15(3).(1, 2) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func16(2).(1, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func17(1).(2, 3) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func18(1).(3, 2) === {1, 2, 3}
    assert MLModuleTest_Specific.testering_func19(1).(:unused, 2, 3) === {1, 2, 3}

And the generated elixir:

  def(testering_func0()) do
    42
  end
  def(testering_func1(a, b, c) when is_integer(c) and (is_integer(b) and is_integer(a))) do
    {a, b, c}
  end
  def(testering_func2(a, b, c) when is_integer(c) and (is_integer(b) and is_integer(a))) do
    {a, b, c}
  end
  def(testering_func3(a, b, c) when is_integer(c) and (is_integer(b) and is_integer(a))) do
    {a, b, c}
  end
  def(testering_func4()) do
    testering_func0()
  end
  def(testering_func5()) do
    testering_func1(1, 2, 3)
  end
  def(testering_func6(a, b, c) when is_integer(c) and (is_integer(b) and is_integer(a))) do
    testering_func1(a, b, c)
  end
  def(testering_func7(a) when is_integer(a)) do
    testering_func1(1, a, 3)
  end
  def(testering_func8()) do
    fn ($var_0, $var_1, $var_2) when is_integer($var_2) and (is_integer($var_1) and is_integer($var_0)) -> testering_func1($var_0, $var_1, $var_2) end
  end
  def(testering_func9(a) when is_integer(a)) do
    fn ($var__0, $var__1) when is_integer($var__1) and is_integer($var__0) -> testering_func1(a, $var__0, $var__1) end
  end
  def(testering_func10(a) when is_integer(a)) do
    fn $var__0 when is_integer($var__0) -> testering_func1(1, a, $var__0) end
  end
  def(testering_func11(a) when is_integer(a)) do
    fn $var__0 when is_integer($var__0) -> testering_func1(a, 2, $var__0) end
  end
  def(testering_func12(a, b) when is_integer(b) and is_integer(a)) do
    fn $var__0 when is_integer($var__0) -> testering_func1(a, b, $var__0) end
  end
  def(testering_func13(a) when is_integer(a)) do
    fn ($var_0, $var_1) when is_integer($var_1) and is_integer($var_0) -> testering_func1(a, $var_0, $var_1) end
  end
  def(testering_func14(a) when is_integer(a)) do
    fn ($var_0, $var_1) when is_integer($var_1) and is_integer($var_0) -> testering_func1($var_0, a, $var_1) end
  end
  def(testering_func15(a) when is_integer(a)) do
    fn ($var_0, $var_1) when is_integer($var_1) and is_integer($var_0) -> testering_func1($var_0, $var_1, a) end
  end
  def(testering_func16(a) when is_integer(a)) do
    fn ($var_0, $var__0) when is_integer($var__0) and is_integer($var_0) -> testering_func1($var_0, a, $var__0) end
  end
  def(testering_func17(a) when is_integer(a)) do
    fn ($var_0, $var_1) when is_integer($var_1) and is_integer($var_0) -> testering_func1(a, $var_0, $var_1) end
  end
  def(testering_func18(a) when is_integer(a)) do
    fn ($var_0, $var_1) when is_integer($var_1) and is_integer($var_0) -> testering_func1(a, $var_1, $var_0) end
  end
  def(testering_func19(a) when is_integer(a)) do
    fn (_, $var_1, $var__0) when is_integer($var__0) and is_integer($var_1) -> testering_func1(a, $var_1, $var__0) end
  end

As you can see, if you skip a placement (as in testering_func19) by using, say, _1 without the _0 then a _0 remains unbound and unused. It also supports from _0 up to _127, easily adjustable if it ends up being too much or too little (how?!).

1 Like