Fork about static typing split from another thread

Yes @OvermindDL1 I’m aware of that. :slight_smile:

Is that I find Elixir’s syntax and features so simple that when I look at the “competition” (Scala, Clojure, F#) - I just can’t find something even more simpler.

Scala is a beast in size and features but, tbh, most of them we already have with Elixir without much of a problem. I think that the problem of Scala is the lack of identity overall around the language and the try of merge and have everything in one single language (from DI to the Reader Monad).

The things I dislike in Clojure are the lack of proper types and the worst error messages I’ve ever seen in my life.

F#, I love F#. F# is a well designed language. Don Syme hit the spot when designing F#. Syntax-wise, it’s great, I think Haskell itself would learn a thing or two about computation expressions. Type system doesn’t get in the way and I find it very “beginner friendly” and practical to use. The sad part of F# is that, well, 95% of .NET devs don’t give a shit about it because they are all in C#.

5 Likes

For note, F# is just OCaml, you can copy/paste a lot of OCaml code into F# and it works fine. Main difference is that F# lacks some OCaml features like the powerful module system (wtf?!) and a few other minor things. You should give OCaml itself a try. ^.^

2 Likes

Yes I know about the similarity between F# and OCaml. :slight_smile:

For me, just a F#-like type system on top of a language that looks like Elixir would be awesome.

1 Like

Well my free time has vanished again for a bit (it ebbs and flows) but you are free to help me on TypedElixir. ^.^

3 Likes

:open_mouth:

Do you have the Repo link? I’ll take a look later.

1 Like

Yep at: GitHub - OvermindDL1/typed_elixir

It is the public side of the brain dump side that I have on private bitbucket, it is still a brain dump, but it is the ‘tested’ side of the brain dump. ^.^

So the code is not pretty at all, but the code that is there should at least ‘work’ to some extent. I have code that allows for importing types from other things and more about typing functions as well as a building set of the ‘or’ type (necessary for elixir, unnecessary and not in most ML style languages like F#) in the more random private repo I have at bitbucket, but I need to move that all to github after I get tests and such made. Feel free to PR though, just always include tests for what is being added. :slight_smile:

2 Likes

Elixir with a layer of strong typing akin to TypeScript for Javascript would be really nice :slight_smile:

4 Likes

I’d like to help out as well. I don’t have much experience along these lines, but I did take a class that implemented a type system in a pared down version of scheme in SML and I enjoyed it. So I’m going to take a look at the code base. My largest concern is that Elixir has a lot more flexibility due to pattern matching and the different processes, but I’m willing to help attack those issues.

1 Like

IMO efforts here would be better placed on expanding Dialyzer to fully support Elixir.

People have different views on types. Static people see things one way, dynamic see them another.

Elixir gives a hybrid right now of strong and gradual typing. Using Dialyzer and typespecs makes it almost static if you want that. The almost is only because it currently works on BEAM files instead of actual elixir code.

Fill the Dialyzer gap and you’ll provide static typing in Elixir for people who want it and it’s already most of the way there.

6 Likes

I understand that point, but Dialyzer isn’t static typing and a lot more would have to be done to support static typing on top of it.

The short explanation is that dialyzer works on top of code to make sure that the programmer realizes certain mis-types of the code and a static language might cover those gaps. Also a fully static system generally works with a compiler to do things so that at run-time those types are guaranteed and thus cut down on run times where types are checked. That’s probably outside the intention of the library, since it requires compiler support, but it’s a cool concept.

I’d like to see something that includes some of the more innovative things coming from Haskell like Liquid Types that provide checks for things like dividing by zero on the type level instead of the value level, but I’m also really naive to all of this stuff so I’m not sure how or if it could work in the elixir world.

1 Like

Also a fully static system generally works with a compiler to do things so that at run-time those types are guaranteed and thus cut down on run times where types are checked.

That’s really where the crux of the discussion is. The only static language I’ve ever come across that even remotely allow fast iterative development is Go and that’s largely because the language was built from the ground up to minimize compile times. When you force static up front, you lose a lot of the development speed that comes from being able to dive in head first and refine your types as you go. That’s why the current Elixir structure is a near perfect balance, in my opinion.

Based on the talk from Jason Voegele at ElixirConf this year (https://www.youtube.com/watch?v=JT0ECYZ9FaQ) he went pretty deep on this subject and after closing up, his only warning about what you can’t get right now was checking on Elixir structs.

In terms of compiler optimizations, the end goal there is usually to squeeze a little bit more out of a benchmark and that’s not really the purpose of Elixir in the first place. There are lots of other languages out there for top-end execution speed. Elixir’s focus is on development time, isolation, concurrency, fault tolerance, distribution and consistency in response times…over a cluster.

The cluster is the big key here because the compiler can’t make any guarantees over a cluster unless the functions that you’re calling have strictly defined types themselves and it knows them. If it thinks it knows them and they don’t fit, there’s an error and that’s where supervisors already intervene. I’m not sure that it’s possible to introduce static typing to Elixir (or Erlang) without creating a load of headaches for clustering. It would be something akin to forcing all JSON API’s to implement an equivalent of WSDL.

Strong basic types, dialyzer, typespecs, pattern matching and guards already exist. If Dialyzer could check Elixir structs, the only the missing would be the compiler optimizations. From most of the benchmarks I’ve seen, I’m not sure the gains are there to warrant the inevitable complications that would come from it. The math already shows that time-to-market performance trumps time-to-execute performance in all but extreme scale cases. Doesn’t show up in benchmarks, but it definitely shows up on balance sheets.

Disclaimer: I am in no way affiliated with the Elixir project. Just my opinion.

2 Likes

I agree with you. I just would like to do it anyways. I have no idea if it would make any difference and I expect the difference it would make is minimal for run time.

1 Like

You laid it all out very well too

1 Like

Fair enough. I don’t want to dissuade anyones desire to improve the language.

1 Like

Thanks. I’m not the kind of person where functionality matters to me, but I do like the critiques as to why it matters to everyone else. It helps to fill the gap. I’m going to watch that video tomorrow because I haven’t seen it before.

1 Like

It is fun to work on, just wish I had more time. ^.^

PR’s are welcome though. The typed_elixir repo has both typed_elixir and elixir_ml but they use the same typing code. Also things like other processes should be black boxing the types in as much as possible, I was planning to walk types through known things like GenServer as well.

Dialyzer already supports Elixir fine though? At least I use it pretty excessively. ^.^

Just typing is not why I am doing this though, I want to generate things based on types (and have the compiler die fast when I do stupid things), things like union types, automatic constructors, typeclass dispatching (via HPT’s), etc… Right now those are huge hacks to do in Elixir (see disc_union and expede’s typeclass projects).

But dialyzer is only a verifier, you cannot really ‘do’ anything with those types… :-/

I actually have the start of code in that, I hold known values and value ranges as much as I can, and if, say, a binding of x is an integer of any unknown integer value (say user input) and you do 42/x then it should puke at you since you did not verify it is non-0 first via an if check, that is all in the plans and the code is there to be used, just need to work more on it. ^.^

Go is not near as fast as compiling as OCaml, and OCaml is a fully HPT, H-M typed system. :wink:

And I entirely disagree that forcing types reduces fast iterative development. Taking OCaml again, you almost do not have to specify any types anywhere, its HM typing is able to infer from usage and access everything and able to inline things properly in assembly to make for a language that compiles near instant and has a speed within a single factor of C while having some of the smallest and most readable code out. It is a prime example of a typing system done right (Haskell is, eh, not so much).

And proper typing helps you do reduce development time (catch errors early, and you do not even have to add types to things!), concurrency (make sure you handle the right messages in the right places), fault tolerance (substantially reduced bug count is a big boon!, but the OTP gives the rest), etc… etc… Typing only helps, it does not hinder at any point in any decent language.

Take OCaml, you can give it an unknown ‘thing’ to its match function and match out the possible types it might be, that is how you do ‘dynamic typing’ there, and it makes sure you match things out properly, this style would already fit in receive style blocks, case’s, etc…

The big boon of typing is not speed (although it does allow an ‘in-the-know compiler’ to do a lot of optimizations) it is to be able to generate code based on types, properly match things, and most most importantly, reduce bugs. Dynamic typing introduces a whole class of bugs (and my main ones) that static typing entirely eliminates, and as I stated before, languages like OCaml barely require putting in any types anywhere, it is fantastic at inferring, and that stuff definitely shows up on balance sheets. :wink:

5 Likes

Haven’t used OCaml. Generating things based on types is where I was going with the JSON to WSDL reference.

What’s an example where generating things based on types would be beneficial in Elixir? My experience with type based generators has always been extreme code bloat.

1 Like

One of the things that I would use most often is just case matching, take this variant in OCaml:

type blah =
| Blorp
| Bleep of int

We can use it like this convoluted example:

let a = Blorp

let b = function
  | Blorp -> 42
  | Bleep i ->  i

let b_app = b a

(Notice still no types specified?)
This will work as expected, b will be 42. :slight_smile:

However, as development continues, this variant is used all over, I find out I need to change Blorp to hold an argument too, so I change the variant to:

type blah =
| Blorp of float
| Bleep of int

Oh no, a compile error!

Line 5, 8: Error: The constructor Blorp expects 1 argument(s),
       but is applied here to 0 argument(s)

Where the similar thing in Elixir (using tagged tuples or structs or whatever) will silently just keep working, breaking existing code.

Or if I wanted to add a case instead of change one, say to this:

type blah =
| Blorp
| Bleep of int
| Bloop of float

I now get this at compile-time:

Line 8, characters 8-55:
Error 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Bloop _

(I compile with many warnings as errors)
So I need to add that case, or I need to add a default case (_ -> whatever, like in Elixir/Erlang), but this makes me look at all the use-sites of it to see if anything else needs changing, things remain ‘safe’, and yet I’ve still not typed anything beyond making the original variant type itself. ^.^

However, what if I need to handle arbitrary variants, where I do not know what they all can be ahead of time and user-code could add their own, say as in a plug-in system! Well that is trivial in OCaml too:

let c = `Vwoop

let d = function
  | `Vwoop -> 42

let d_app = d c

(The ` marks a polymorphic variant)
Here this match call accepts any polymorphic variant with the name Vwoop with no args, however it will error at compile-time if you pass in something else, like this:

let c = `Dwoop

let d = function
  | `Vwoop -> 42

let d_app = d c

This causes this error:

Line 16, 14: Error: This expression has type [> `Dwoop ]
       but an expression was expected of type [< `Vwoop ]
       The second variant type does not allow tag(s) `Dwoop

Here you can also see how types are inferred, it is saying that the expression will pass in a `Dwoop but potentially open to more types in the future, and the function is accepting any polymorphic variant including `Vwoop or less, however the `Dwoop is not in the allowed list of `Vwoop, but we can fix that with a default:

let c = `Dwoop

let d = function
  | `Vwoop -> 42
  | _ -> 0

let d_app = d c

This works, and you can pass in any type, user-defined, any arguments, etc… These are great for event callbacks of arbitrary events for example, handle what you want, ignore the rest, all fully and entirely type-safe with minimal code. :slight_smile:

Nicely enough, a polymorphic variant in OCaml is like a tagged tuple in Elixir/Erlang, even implemented the same way, `Vwoop would be :Vwoop in Elixir, and `Vwoop 42 would be {:Vwoop, 42} in Elixir. ^.^

OCaml also has when expressions, just like Elixir, so you could make the above b be this for example:

let b = match a with
  | Blorp -> 42
  | Bleep i when i > 0 -> i
  | Bleep i -> -i

Doing the same in Elixir means using something like the disc_union library, which gives you a new ‘case’ that operates over a defined set of variants, but there is no polymorphic variant support. Thus you basically get a lot of code generated ‘for free’ just be virtue of the type system existing (even though you do not specify types, it can infer them, or will complain if it cannot, which almost never happens). This saves substantially on code, thus making it shorter and more readable, fully type safe, and saves substantially on maintenance later.

And of course you can turn those above errors to warnings if you want to ‘iteretively’ add things later but still have it yell at you but be successful (Match_failure at run-time then, which is identical to Elixir’s MatchError). ^.^

Now as for generating ‘new’ code, that is the domain of PPX’s in OCaml (think Macro in Elixir, bit more difficult to write, but technically a lot more powerful, its more like a token macro if one existed in the elixir world), and as an example yes, there is a JSON generator as one example (well more than one, I’ll use yojson here as it is the one I am most familiar with and is the most used one in OCaml, like poison in Elixir, but faster and more powerful, I’ll show how it can work on a variety of types here):

type an_integer = int [@@deriving yojson]

type a_string = string [@@deriving yojson]

type a_record_object = {
  a_string : string;
  an_integer : int;
}  [@@deriving yojson]

type non_derived_record = { blah : string }

type a_variant =
| Blah
| Blorp of a_record_object
| Bloop of non_derived_record
[@@deriving yojson]

And this ppx will generate two functions per each of the tagged types, so for the a_variant type it will generate two functions with the type signatures of:

val a_variant_of_yojson : Yojson.Safe.json -> (a_variant, string) Result.result
val a_variant_to_yojson : a_variant -> Yojson.Safe.json

So it gives you a convert to and a convert from. The convert to the type returns a result of that type or a string containing the parse error. It will recurse through the type as necessary to build up everything in a fully type safe way or it will give you a good parse error. You can also derive from to_yojson or of_yojson instead of just yojson to get only one or the other function instead of both. You can add other tags (tags themselves are not PPX’s, the PPX’s can work over the file and this one just happens to look for tags to do its work, very LISP’y in power) to key things like in the doc examples:

type geo = {
  lat : float [@key "Latitude"];
  lon : float [@key "Longitude"];
}
[@@deriving yojson]

That way the json key types do not have to match the type name, and there are others to handle encoding, put a default for if it does not exist, etc… etc… You can of course use raw yojson methods to parse into or out of json manually, but if you have a set schema then this makes it trivial to safely parse out a json to exactly what it should be, try that with Elixir’s Poison. :wink:

5 Likes

Wow. There’s a lot to talk about there and I probably need a primer on OCaml to fully understand those examples.

Just to highlight this result from the earlier example:

Line 8, characters 8-55:
Error 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Bloop _

Doesn’t that seem like something that Dialyzer could do with a strictness flag? We know that Dialyzer currently only bothers you if it knows there’s a problem vs where there might be a problem. It seems like it should be possible to modify it to warn about unhanded cases if that was something that you wanted/needed your code to address since it already tracks them and just decides not to say anything.

I’ll reread and try to dive back in later tonight though.

2 Likes

A big problem is that it cannot know about dynamic cases like in the polymorphic variant example, as well as what if the type of the variant argument changed, it would not catch that either in a lot of cases, as well as most of what dialyzer can do goes out the window with dynamic calls, and these are just the base things. Dialyzer is awesome, but it is a positive typer, not a negative typer, and that means that there are a lot of cases that it just categorically cannot handle, but it is pretty necessary if you want to process unmodified Erlang/BEAM due to how dynamic the BEAM is. You can easily make a statically strongly typed language compile on a dynamically typed, but you cannot easily make a dynamically typed run on a statically typed, which is what dialyzer does, hence why I think it is best to have something strong compile ‘to’ elixir/erlang/beam rather than trying to type it directly. Like Elixir could have been strongly typed (with even fewer typespecs and types than it has now too!) fairly easily if it was designed with it in mind, where I’m compensating in my projects by basically error’ing on parts I do not yet handle in elixir, so it is a strict subset, but it can grow over time to potentially reach all of elixir, but it may require some slight coding changes, unlike dialyzer. :slight_smile:

2 Likes