Elixir in the Type System Quadrant

Programming languages are created with different kinds of type systems. When “Thinking Elixir”, it is helpful to know where Elixir sits in the Type System Quadrant and where that is relative to the language you are coming from. Beyond coming from OO, you may also be coming from a different area of the Type System Quadrant. It is helpful to understand the differences so you can better appreciate how Elixir may be different than what you’re familiar with.

Type System Quadrant

Let’s look at the Type System Quadrant with some languages placed in their respective areas. We’ll explain what it means next.

The horizontal axis deals with when type checking happens in the language. From left to right, it goes from static to dynamic.

  • Static: type checking at compile time
  • Dynamic: type checking at run time

The vertical axis is for type safety. From top to bottom, it goes from a weak type system to a strong type system. This describes how aggressive a language is with enforcing its type system.

  • Weak: allows implicit conversions that lose precision
  • Strong: requires explicit conversions where precision would be lost

If you would like to lookup other languages, you can find them loosely categorized here.

Which area of the quadrant is best?

Which is best? There is no “absolute good” here. They behave differently and this results in pros and cons when using a language for a specific purpose.

It is worth being aware of what you are accustomed to and where you are coming from when coming to Elixir.

Why is C considered weak?

C is a static language. The type checking happens at compile time. However, a major feature of the language is pointers. At run time, the program can be told to “interpret this blob of memory as a float”, but it could have been allocated and assigned ASCII character data. No run time checking is performed. The binary memory chunks will now be interpreted as a float. This behavior has caused no end of security vulnerabilities. This characteristic (and consequences) gave rise to the Rust language. The goal being to create a type safe C-like language.

A look at JavaScript

JavaScript is a weak dynamic language. This is an easy one to play with as you can open a browser console and try this out.

JavaScript is very permissive in converting types. It implicitly converts types, not always how you would expect either!

"1" + 2
//=> "12"

2 + "2"
//=> "22"

{} + []
//=> 0

[] + {}
//=> "[object Object]"

How does object + array = 0? Then flipping it around, array + object = object? Wacky.

The JavaScript runtime implicitly converts those types for you.

Elixir is strong dynamic

Elixir has a strong type system. Try these operations out in IEx. Elixir will not implicitly convert one type to another type where precision is lost.

"1" + 2
#=> ** (ArithmeticError) bad argument in arithmetic expression: "1" + 2
#=>     :erlang.+("1", 2)

["a"] ++ ["b"]
#=> ["a", "b"]

{"b", 1} ++ ["a"]         
#=> ** (ArgumentError) argument error
#=>     :erlang.++({"b", 1}, ["a"])

{"b", 1} + ["a"] 
#=> ** (ArithmeticError) bad argument in arithmetic expression: {"b", 1} + ["a"]
#=>     :erlang.+({"b", 1}, ["a"])

Elixir has dynamic type checking. It happens at run time. This is evident in pattern matching.

{:ok, %{name: name}} = {:ok, %{name: "Howard", age: 25}}

name
#=> "Howard"

At run time, the BEAM checks the types. Here’s a hypothetical walk-through of the type checks:

  • Is the left side of the match operator a 2 element tuple? Yes.
  • Is the first element the :ok atom? Yes.
  • Is the second element a map? Yes.
  • Does the map have an atom :name key? Yes.
  • Then bind the value from the right to the name variable on the left.

Notice that it goes beyond just checking the types. It is also comparing specific values as well. As these checks all happen at run time, it is dynamic.

Pattern matching is a feature I absolutely love in Elixir. I don’t believe you can have the expressive pattern matching you find in Elixir in a static language.

If you are coming from a static language and the lack of compile time type checking is uncomfortable, I understand. Try to keep in mind that this more dynamic type checking enables features you could not otherwise have. Try to keep an open mind.

I don’t believe you can have the expressive pattern matching you find in Elixir in a static language. The type checks must happen at run time.

Expanding Elixir towards static

Within the Type System Quadrant, each language can’t be easily reduced to a single point in a grid. A feature of the language or ecosystem might define a point. All together, a language is more of a point cloud. Additionally, the features you choose to use (or not use) in your project push it in a more static or dynamic direction.

For instance, Elixir has optional language features and ecosystem tools that extend its area towards the static side. These include:

  • Structs have compile time checks applied to keys
  • Behaviour implementations are checked at compile time
  • Dialyzer is a “static code” analysis tool
Optional features extend Elixir towards the “static” side.

Closing

Elixir is a strong dynamic language. Types are checked at run time (dynamic) and the enforcement of type conversions is strong.

If you are coming to Elixir from a static language, it may feel uncomfortable. You may miss the strict compile time type checking. Just keep in mind that some of the greatest strengths and features of Elixir are possible because of these type system choices.

Also, the use of specific language features and ecosystem tools can push your Elixir project further in the direction of static if that’s what you want.

6 Comments

  1. taiansu on February 27, 2020 at 9:19 am

    >I don’t believe you can have the expressive pattern matching you find in Elixir in a static language. The type checks must happen at run time.

    Haskell.

    • brainlid on February 27, 2020 at 12:26 pm

      Haskell is a great example of a strong/static Functional Programming language.

      I admit what it means to be “expressive” is a matter of perspective as well. Specifically I was thinking of things like Row Polymorphism which I use in Elixir. This is where you can pattern match on the shape of the data and not strictly on the type. Haskell does not support this but there are proposals to try and add it.

      For me, expressing the shape (not specifically the type) of the data is more expressive. That happens at runtime. Likewise, the explicitness and strictness of the types in Haskell may be more expressive to you.

      Thanks for your comment!

      • taiansu on February 27, 2020 at 4:18 pm

        Thanks for the reply!

        My main concern is for the “The type checks must happen at run time.”. Actually even in Elixir, pattern matching still happens in compile time. You can try

        foo = fn
          (1, x) -> x
          (2, x) -> x + 10
        end
        foo.(3, 100)
        

        then elixirc your_file.ex, you’ll see what you got is a “Compilation error”

        Secondly, when we talking about the shape of the data, actually it’s some sort of “type”. In Haskell, data shape can be represented in very flexible ways. Here are some examples what Haskell can do for a long time. The link you post, if I understand correctly, is to add a syntax sugar to a new type. And my two cents is, on the topic of pattern matching, Haskell, F#, OCaml can do more since you have static type checking.

        Don’t get me wrong, I love Elixir (and Haskell, too). The thing is static type checking is a powerful tool, and I understand the reason Erlang choose the optimistic type checking (unsound, in other words) base on it’s “let it crash” philosophy. But from time to time, I still wish for a well-typed language on BEAM.

        • brainlid on February 27, 2020 at 4:40 pm

          Ah. Thanks for your detailed response. Interestingly, using elixirc to run a file isn’t the normal approach to a project. Your example does in deed result in a “Compilation Error”. However, if you put that same code into a project and compile the project, it won’t error until it is executed. So it compiles it but the types aren’t checked until runtime.

          Here’s an example of Row Polymorphism in Elixir. Using your Point3D example, I can create a single function that handles Point2D and Point3D. My pattern specifies the type is a map but defines the shape as having specific keys.

          I agree that there are many compelling benefits to static type systems and languages. I’ve developed in multiple static and multiple dynamic languages. There are also drawback to both as well. It’s all trade-offs. I know there are efforts to have more static typed languages on the BEAM and I am very interested in seeing what comes out of that. Thanks for your feedback!

          • taiansu on February 28, 2020 at 2:34 am

            Thanks! I totally agree with your opinion on the static vs dynamic typing system are trade-offs.

            But Elixir and Erlang do pattern matching on compile time. The sample code I type indeed error out while trying to mix compile.

            https://d.pr/i/sqHmVt



          • brainlid on February 28, 2020 at 6:01 am

            I think there is some confusion about how Elixir project code is compiled and executed. Your example code is being executed and evaluated at compile time but it isn’t a valid example of how Elixir code and projects work. I built an Elixir demonstration project and the README documents and explains what is happening and when it happens.

            Github demonstration project

            Additionally, it shows how Dialyzer can be used for static code analysis and identify the problem in the code.



Leave a Comment

You must be logged in to post a comment.