Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

case on Sum types #15

Open
OvermindDL1 opened this issue Jul 5, 2017 · 4 comments
Open

case on Sum types #15

OvermindDL1 opened this issue Jul 5, 2017 · 4 comments

Comments

@OvermindDL1
Copy link

It would be nice to also generate a case macro for a sum type that can validate that all options are tested for (so if we extend it later we do not forget a place to update), such as with an example of:

defmodule Light do
  defsum do
    defdata Red    :: none()
    defdata Yellow :: none()
    defdata Green  :: none()
  end
end

And could use it as:

def myfunc(light) do
  Light.case light do
    %Red{} -> "red"
    %Yellow{} -> "yellow"
    %Green{} -> "green"
  end
end

Then if I changed the original sum type to be:

defmodule Light do
  defsum do
    defdata Red    :: none()
    defdata Yellow :: none()
    defdata Green  :: none()
    defdata Custom do
      red   :: non_neg_integer()
      green :: non_neg_integer()
      blue  :: non_neg_integer()
    end
  end
end

Then the prior Light.case would throw a compile-time error stating that not all branches are taken, of which it could be fixed by either adding a:

    %Custom{red: r, green: g, blue: b} -> "custom<#{r}:#{g}:#{b}>"

Or (though this form would swallow all future modifications as well, thus not recommended, but allowed, maybe to allow this an option has to be passed to the Light.case to allow a default case?):

    _ -> "unknown"

I'm just wanting anything that forces me to handle modified sum types instead of just silently ignoring them thus allowing me to forget about them. :-)

P.S. Love the design! I might toss this into my MLElixir playground (definitely will with the above change!) as it would simplify my manually created sum/prod types. ^.^;

Also, I notice the prod types are structs, if they were matched on in, say, the Light.case example above then I'd definitely want it to error if not all subtypes are handled, thus if Custom was changed to have an alpha with a default value, then in the Light.case the alpha has to be matched as well or it will fail compilation (at least one case should check 'all' values, even if earlier cases only match a couple), or perhaps (maybe via an option on the case as well) allow an empty matcher for the type like %Custom{} = custom so that it assumes the user will access it properly internally or so, unsure on exact semantics that I think would be best, would have to play with it...

@expede
Copy link
Member

expede commented Jul 5, 2017

Hm, yeah, I definitely agree that's a useful feature! I was under the impression that Dialyzer does missing cases analysis, no?

@OvermindDL1
Copy link
Author

Hm, yeah, I definitely agree that's a useful feature! I was under the impression that Dialyzer does missing cases analysis, no?

It misses a whole ton of cases. Dialyzer is only a success typer, so if it tells you something is wrong then it is definitely wrong, but if it is quiet then something 'might' be wrong or it 'might' be right, and in regards to cases it does not get everything. >.>

/me wishes Elixir had Hindley-Milner Typing...

@expede
Copy link
Member

expede commented Jul 6, 2017

/me wishes Elixir had Hindley-Milner Typing...

Yup, me too! (Actually, I wish that I could work in a dependently typed language, but le sigh). With luck, Alpaca will save us all (and add explicit type signatures) 😉

@OvermindDL1
Copy link
Author

I'm actually more partial to OCaml style typing, it is easier to compile, faster to resolve, plus if you bring in the module system (which is entirely doable on the BEAM) then it becomes more powerful than what you can do in Haskell style. :-)

I'd been playing with seeing how I can stretch the Elixir AST in macro's and as I get time I've been screwing around with an HM-typing mini-macro-language, can see the tests at (you get type unification failures and all properly if something is mis-typed, and as you can see you can 'type' a normal elixir call on an elixir module so the system understands and can use, it adds the right guards and all to make sure normal elixir cannot mis-call something):
https://github.com/OvermindDL1/typed_elixir/blob/master/test/ml_elixir_test.exs
https://github.com/OvermindDL1/typed_elixir/blob/master/test/ml_elixir/ml_module_test.exs

In general you can do this and it generates valid, fully typed elixir (notice even can 'type' modules as in SML/OCaml ;-) ):

import MLElixir

defmlmodule MLModuleTest do

  type type_declaration

  type type_definition = integer

  type record_emb_0 = %{a: %{b: %{c: type_definition}}}

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

  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(a) = a
  def identity_typed(b | !id_type) | !id_type = b
  def identity_int(c | integer, f | float) | integer = c
  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
  def test_noblockT1(x | !id_type) | !id_type = x
  def test_record | record_emb_0 = %{a: %{b: %{c: 42}}}
  def test_record_sub(r | record_emb_0) = r.a

end


defmlmodule MLModuleTest_Generalized do
  type t

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



  defmlmodule MLModuleTest_Specific, debug: [:_resolving, :module_pretty_output] 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
    # | t integer # Single value?
    # | t_f0 integer, float # multiple value?
    # | i_f1(integer, float) # Hmm, what for...
    # | t_f2{integer, float} # Hmm... default tuple?
    # | t_f3[i: integer, f: float] # Keyword list version maybe?  Or just make a normal list?
    # | %t_f4{t: integer, f: float} # Map version?
    | tuple_enum0{}
    | tuple_enum1{integer}
    | tuple_enum2{integer, float}
    | tuple_recurception{{integer, float}}
    # | record_enum %{integer: integer}
    # | map_enum %{integer => float}
    # | gadt_enum = (integer | 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
    def testering12 = MLModuleTest.test_int_untyped
    def testering13 = MLModuleTest.test_int_typed
    def testering14 = MLModuleTest.test_record.a.b.c
    def testering15(r) = MLModuleTest.test_record_sub(r).b.c
    # def testering16 = MLModuleTest.testering_enum # Should fail
    def testering17 = MLModuleTest.testering_enum.one
    def testering18 = MLModuleTest.testering_enum.integer # auto-curry gadt head test
    def testering19(i) = MLModuleTest.testering_enum.integer i
    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
    def testering26 = testering0
    def testering27 = tuple_recurception {42, 6.28}

    # Functions
    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
    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
    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
    def testering_func30(f) = f(2)
    def testering_func31(value, f) = f(value)
    def testering_func32(value | integer, f) = f(value)
    def testering_func33 = testering_func31(3, testering_func1(1, 2))

    # Bindings
    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}

    # Records (I.E. Erlang/Elixir atom() keyed maps, like structs)
    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, +: record0, z: integer}
    type record_ex_2(t) = %{+: record2(t), z: integer}
    type record_ex_2_float = %{+: record2(float), z: integer}
    # type record_ex_sub(t) = %{+: t, z: integer} # Need to support unbound's perhaps? # Unsure if I want to support this...
    type record_rem_0 = %{+: record1, -: x}
    type record_emb_0 = %{a: %{b: %{c: 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}
    # def testering_record9 | record_ex_sub(record2(float)) = %{t: 6.28, z: 42} # Unsure if I want to support this...
    def testering_record10 | record_rem_0 = %{y: 6.28}
    def testering_record11(r | record_emb_0) = r.a.b.c

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

    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 42
    def testering_tuple12 | tuple3 = {42, {6.28, 42}}

    # FFI
    external addi(integer, integer) | integer = Kernel.+
    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)

    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)
    # def testering_op_pipe2(i) =
    #   42
    #   |> identity
    #   |> testering_func1(1) # partial currying not yet implemented, see:  :make_function_wrapper_of_proper_length
    # def testering_op_pipe2() =
    #   42
    #   |> identity
    #   |> testering_func1
  end

I'm not intending on that to be a real language, just me screwing around for fun, if I were to make a real language in it then it would not be constrained by the Elixir AST for sure. ^.^;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants