The use and over-use of type variables

The thread on limiting the number of arguments parameters in the tags of custom types was getting derailed by an interesting conversation about the (over) use of type variables. I was curious to learn more about people’s experiences so I’m creating this as a place to move the discussion :smile:

For reference, a type variable is like the a in:

type Maybe a
  = Just a
  | Nothing

A few questions to get this started. Feel free to answer some/none/all of these.

  1. What are some benefits of using type variables? What are some drawbacks?
  2. What contexts most benefit from type variables? elm-core, library code, app code
  3. What are examples of situations where you’ve found type variables improved your code?
  4. What are examples of situations where you regretted using type variables?
  5. Do you have a personal guideline on when you use type variables?
1 Like

Cool. Sorry if I derailed that other thread.

My only real reason for potentially advocating against the use of type variables is that it:

  1. Is one of the only areas in Elm that I know of where you are possibly opening yourself up to odd behavior in Elm, given that the compiler has to do more work.
  2. Makes it potentially harder for someone to understand what your intent was vs. if you’d use a custom type.

Meaning, the only way to understand how I’m using a function with a type variable is to search through the code and see how the function is being used in practice, so I can extrapolate from there.

If instead, I have a function that uses a custom type in the function signature, I can just search for the custom type, then I am guaranteed that I understand the inputs and outputs of that function, and can use it with confidence.

Does that make sense?

Again, type variables are critical for the core code, e.g. the Maybe type, List, etc. No question about that!

For example, from elm-visualization 2.0 change log:

  • Change Visualization.List.range from number -> number -> number -> List number to Float -> Float -> Float -> List Float . This would enable much simpler and more correct code (partially this is due to compiler bugs, but not entirely).

I might just take a stab at re-writing the Elm SPA example just to show what I mean. That way folks can decide if what I’m saying makes a lick of sense!

As I said, I only mentioned it in the first place because we have a ton of Elm code and no type variables at all, and inevitably the first thing I do if I get code w type variables in it is to replace them with custom types to make sure I really understand the code.

From another thread:

In an application that I am working on we have a pagination “component” that is used in many lists. It takes in a (Int -> msg) (note the lowercase msg ) as a message constructor from the enclosing module to produce when either a number or arrow button is pressed. Without type variables, you’d have to intercept this specific message from the pagination update but forward all of the rest of its messages on to it.

Because this msg must come from outside, it must be specified. And because it must be specified, it cannot be forgotten in your enclosing update function. If you take these two parts away, it becomes less clear how to use the component. Your application will compile just fine and you will get no feedback as to why your button clicks do nothing.

So what I’m realizing is that the idea of not using type variables might make zero sense to folks who build Elm apps using a “component” model!

This is how we write apps:

This is a highly opinionated style of writing large-scale Elm apps. I will say that it’s battle-tested and super easy to onboard new devs, but may not be for everyone, especially if you come from a React/component kinda mindset.

So all view functions have exactly the same type signature:

viewThing : Model -> Html Msg

and we have only one single (massive!) update function:

update : Msg -> Model -> ( Model, Cmd Msg )

and only one, single Model. See the github link for more details.

Outside of core, type variables are essential for any generalized data structures you might write as libraries. I don’t need to know or care what kind of data is associated with the nodes in a tree, for example, in order to code up all the tree navigation and manipulation operations that are needed to work with tree. Here is a tree data structure I wrote:

tea-tree src on github

Similar can be said for structuring computations which always follow the same pattern, but where some of the implementation details can be abstracted out, so that the same structure can be re-used in many different situations. Here are examples for working with nested updates, and for generalized searches over graphs:

elm-update-helper src on github
ai-search src on github

The above examples are all from packages where some code has been generalized for re-use in different situations. The type variables allow unknown behavior to abstracted out and combined with the known code in the package. This allows more sophisticated tools to be constructed from their parts without having to go back to beginning each time. In brief, it is one of the mechanisms through which ‘software engineering’ can be done.

To give an example from an application instead of a package, here are some type declarations from one of my applications:

type alias LinkBuilder msg =
    String -> Html.Attribute msg

type alias Template msg =
    LinkBuilder msg -> Editor msg -> Zipper Content -> Html msg

type alias Layout msg =
    Template msg -> Template msg

type alias Editor msg =
    Zipper Content -> Html msg

I have made use of both type variables and higher order functions here. Higher order functions (call-backs) are another excellent mechanism through which changeable program behavior can be combined together in a pluggable way; you implement some code, then use the function to inject variable behavior. Probably the most well-known example of this is List.map. This is a loop (the known behavior) combined with a function (the unknown behavior), and the caller can inject whatever map function they like but never has to write a for (int i = 0; i < length; i++) loop again.

To come back to my application - I have 2 types of Editor. One implementation is a content editor that lets me edit content as markup, and the other is not really an editor at all, it is just a markup renderer. So this particular application has 2 modes of operation, read mode and edit mode. In read mode msg gets bound to Never and in edit mode it gets bound to a Msg type that captures all of the edit actions.

I could capture those 2 possibilities as a custom type with 2 options, but I would rather not. None of the code that works with Templates and Layouts needs to know anything about how the editors might work. Using type variables eliminates the possibility that this code might bind to things it should know nothing about. I also plan to add more types of editors to my application, perhaps one for adjusting images for example.

===

  • What are some benefits of using type variables? What are some drawbacks?

A very important software engineering aspect of the language. Many things are simply not possible without them.

  • What contexts most benefit from type variables? elm-core, library code, app code

Mostly core and library code, I agree. But I have also shown how they can be used in conjunction with higher order functions to structure more complex applications.

  • What are examples of situations where you’ve found type variables improved your code?

Not just improved, but made things possible which are not possible otherwise.

  • What are examples of situations where you regretted using type variables?

Sometimes I have experimented with structures like this:

type alias TextDiagram a =
    { a
        | labels : List PathSpec
        , pathsForLabels : EveryDict PathSpec Path
    }

src on github

The idea being that the caller can embed the fields my package needs in some record in their application. A few other packages also did this kind of thing, elm-mdl is one that comes to mind. It worked fine, but I think was an unnecessary complication and would have been better a different way.

  • Do you have a personal guideline on when you use type variables?

As I say, whenever I need to abstract out some behavior to re-use code, and when building a scaffolding for a more complex application.

2 Likes

Interesting discussion! Personally I find type variables very useful, and in many situations I feel like they can make code easier to understand. For example, if I see:

viewWidget : Title -> Size -> Html msg

…I immediately know that the widget will not produce any messages - i.e. it’s visual only, not interactive. If the return type were Msg, I could no longer tell that just by looking at the type signature.

Along the same lines, if I see:

beginningWith : a -> History a

…I know that the History structure doesn’t know or care about the values it’s storing. In my mind this makes it easier to understand, as it limits the scope of what it can do, and means it can be understood in isolation from the rest of the application.

On the other hand, I can see how overuse of type variables (and in general, unnecessary abstraction) can make code harder to grok.

3 Likes

To me there’s a very obvious pattern: I use type variables (generic code) very rarely in apps that I write, but they are widely used and seem pretty much indispensable in packages (and not just those in core). Without the ability to write generic code, you couldn’t have elm-ui or remotedata or elm-css.

6 Likes

Well said, makes a lot of sense.

ps I remember playing around with elm-mdl. An ambitious project!

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.