One of Elm’s most important characteristics is its static type system. This enables Elm to make much stronger guarantees during run time compared to dynamic languages like JavaScript. This boils down to “If it compiles, it’ll never throw a runtime exception”. In this episode, we’ll look into the type system and on type annotations in particular more closely.
About This Series
This is the sixth post in a series of short and sweet blog posts about Elm. The stated goal of this series is to take you from “completely clueless about Elm” to “chief Elm guru”, step by step. If you have missed the previous episodes, you might want to check out the table of contents.
Type Annotations Versus Type Inference
Although Elm is a statically typed language, our examples in the previous posts had no type declarations whatsoever. The reason is that Elm can infer the type of almost any expression. That means that everything is typed, either explicitly by adding a type annotation or implicitly by relying on Elm’s type inference.
Let’s revisit some of the functions from the last episode and add type annotations to them. The type annotations are the line just before the function definition:
import Html multiply : number -> number -> number multiply a b = a * b square : number -> number square a = multiply a a productOfSquares : number -> number -> number productOfSquares a b = multiply (square a) (square b) incrementAll : List number -> List number incrementAll list = List.map (\ n -> n + 1) list incrementAll2 : List number -> List number incrementAll2 = List.map (\ n -> n + 1) main : Html.Html main = let print n = Html.text <| toString n in Html.p [] [ print <| multiply 3 5 , Html.br [] [] , print <| square 4 , Html.br [] [] , print <| productOfSquares 2 3 , Html.br [] [] , print <| incrementAll [1, 2, 3] , Html.br [] [] , print <| incrementAll2 [1, 2, 3] ]
(Remark: I refactored the main function a bit from the last episode by extracting the duplicated conversion from number/list into an HTML text element.)
To pick one example, square : number -> number
is the type annotation for the function definition of square
. A type annotation should be written directly in the line above the function definition. It is comprised of the function name, a colon, and the types of all input parameters and the return type, each separated by ->
.
There is no distinction between a parameter type and the return type. Coming from other languages, you might expect that the type annotation for multiply
somehow makes it clear that two numbers go in and one number comes out. For example the type signature could read (number, number) -> number
. It doesn't. The separator between the first and second input parameter is ->
, just as the separator between the second input parameter and the return type. This is because talking about first and second input parameter is just one way of thinking about multiply
. You could also say that multiply
takes only one parameter and returns a function with the signature number -> number
. Both are equally valid points of view. You can either think of this function as (number, number) -> number
or number -> (number -> number)
. The reason is that Elm supports currying and partial function application naturally.
Here is a code example that illustrates this:
import Html multiply : number -> number -> number multiply a b = a * b multiplyByFive : number -> number multiplyByFive = multiply 5 -- The expression (multiply 5) yields a new function with signature -- (number -> number) by partial appication, that is: the first argument to -- multiply is provided, but not the second. main : Html.Html main = multiplyByFive 3 |> toString |> Html.text -- As you probably have guessed, the output of this snippet is 15.
Composing Types
Let's also have a look at the other two functions from the previous episode (and their type annotations):
incrementAll : List number -> List number incrementAll list = List.map (\ n -> n + 1) list incrementAll2 : List number -> List number incrementAll2 = List.map (\ n -> n + 1)
These functions work with lists, but when talking about lists it is also important which type the elements in the list have. Therefore the full type here is not simply List
but List number
, that is, a list of number
elements. This is just an example for the more general concept of parameterized types. This is mostly used in containers (like List or Maybe) and usually defines which type the contained elements have.
Generating Type Annotations Automatically
You can even let the Elm compiler tell you the type annotations. If you have Elm code without type annotations and you compile it with elm-make
with the --warn
flag, it will tell you all type annotations it inferred.
Going back to the example from the previous post (which had no type annotations), here's how that looks like:
elm-make --warn Functions.elm
=================================== WARNINGS =================================== -- missing type annotation -------------------------------------- Functions.elm Top-level value `multiply` does not have a type annotation. 3│ multiply a b = a * b ^^^^^^^^^^^^^^^^^^^^ I inferred the type annotation so you can copy it into your code: multiply : number -> number -> number
Isn't that nice? I think it is.
Conclusion
Adding type annotations to your programs or omitting them is a matter of taste and style. However, the Elm style guide recommends having type annotations on all top level definitions. In my experience they often make it easier to solve compiler errors. And, to be quite honest, you'll do a fair share of fighting compiler errors when working with Elm. Therefore it usually pays off to add type annotations to your functions.
There is much more to Elm's type system then what have covered today -- union types, type aliases, type variables, tuples and records to name a few – but those will be topics for another Elm Friday.
This concludes the sixth episode of this blog post series on Elm. Continue with the next episode, that is all about lists, a central data type in Elm.
The post Elm Friday: Type Annotations (Part VI) appeared first on codecentric Blog.