# Compile-time checks on values in a list passed as an argument

**URL:** <https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796>\
**Category:** Request Feedback\
**Created:** [May 19, 2020, 10:26am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796 "2020-05-19T10:26:21Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![alexkorban](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/alexkorban/32/3242_2.png) [@alexkorban](https://discourse.elm-lang.org/u/alexkorban)\
**Post date:** [May 19, 2020, 10:26am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/1 "2020-05-19T10:26:21Z")

</div>

After reading @jfmengels’s [post](https://jfmengels.net/single-out-elements-using-phantom-types/) about using phantom types to single out an element of a type, I was curious to see if I could apply this technique to constrain a list of values passed as an argument.

[In this post](https://korban.net/posts/elm/2020-05-18-compile-time-checks-values-lists-phantom-types), I explored two different variations (nested type tags and extensible records) and came to a (probably unsurprising) conclusion that extensible records are more flexible.

Extensible records make it possible to get the compiler to enforce that only _supported_ attributes are passed in a list.

But ideally, I’d like to be able to enforce two types of constraints on a list of values given to a function:

- required values are supplied in the list
- only supported values are supplied.

This would allow for a nice API in situations where, for example, a function takes a list of attributes or a list of child elements etc.

However, I wasn’t able to find a way of enforcing both of those with phantom types, so my current thinking is that the best solution is to have two arguments: a record with required attributes as the first argument followed by a list of optional attributes which the compiler rejects if they’re not supported by the function according to phantom type tags. This is similar to the `elm-ui` API in `Element.Input`, for example (minus the phantom types).

Is it possible to have a better API than this? I’d be keen to hear of ways to enforce more compile-time checks.

---

<div class="post-metadata">

**Author:** ![dmy](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dmy/32/702_2.png) [@dmy](https://discourse.elm-lang.org/u/dmy)\
**Post date:** [May 19, 2020, 11:23am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/2 "2020-05-19T11:23:00Z")

</div>

> [@alexkorban](#):
>
> But ideally, I’d like to be able to enforce two types of constraints on a list of values given to a function:
> 
> - required values are supplied in the list

If it was possible, it would be a kind of `NonEmptyList`, wouldn’t it? Which is not possible with a simple `List`.

* * *

If you are ready to give up on the list, a close alternative that allows to enforce both constraints is a function composition API with phantom types like this:

```elm
rect ( width 1 >> height 2 )

```

For example for a type like:

```elm
type Shape
    = Rect (List (Attr { width : Set, height : Set }))

```

You can have attribute constructors like:

```auto
width :
    Int
    -> List (Attr { compatible | width : Required })
    -> List (Attr { compatible | width : Set })

height :
    Int
    -> List (Attr { compatible | height : Required })
    -> List (Attr { compatible | height : Set })

```

then you create a rectangle with a function `rect`:

```elm
rect :
    (List (Attr { width : Required, height : Required })
     -> List (Attr { width : Set, height : Set })
    )
    -> Shape

```

for example:

```elm
rect ( width 1 >> height 2 )

```

You get compiler errors if:

- You forget one of `width` or `height`
- You set twice a `width` or a `height`
- You set an unsupported attribute

You can also notice that the API is actually used almost exactly like the list one, except `[` and `]` is replaced by `(` and `)`, and `,` by `>>`.

A drawback though is that if all arguments are optional, you have to pass `identity` instead of `[]`, which is less intuitive.

You can add attributes supported by any shape, like:

```elm
color : String -> List (Attr a) -> List (Attr a)

```

For example:

```elm
rect (width 1 >> height 2 >> color "blue")

```

Or optional attributes supported by a subset of shapes, for example:

```auto
type Shape
    = Rect (List (Attr { width : Set, height : Set, rounded : Supported }))

rounded :
    Int
    -> List (Attr { compatible | rounded : Supported })
    -> List (Attr { compatible | rounded : Supported })

```

Here is a full example with several shapes to play with:  
[https://ellie-app.com/8TKSpYH8TNfa1](https://ellie-app.com/8TKSpYH8TNfa1)

---

<div class="post-metadata">

**Author:** ![dmy](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dmy/32/702_2.png) [@dmy](https://discourse.elm-lang.org/u/dmy)\
**Post date:** [May 19, 2020, 11:32pm UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/3 "2020-05-19T23:32:15Z")

</div>

Here is another example with an API for `padding` and `margin` that guarantees that each edge has been set exactly once, whatever the function used:

```elm
padding (all 10)
padding (horizontal 5 >> top 10 >> bottom 20)
margin (horizontal 15 >> vertical 25)

```

Any forgotten or duplicated edge leads to a compiler error:

[https://ellie-app.com/8TRyBYvGXx3a1](https://ellie-app.com/8TRyBYvGXx3a1)

---

<div class="post-metadata">

**Author:** ![alexkorban](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/alexkorban/32/3242_2.png) [@alexkorban](https://discourse.elm-lang.org/u/alexkorban)\
**Post date:** [May 20, 2020, 12:13am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/4 "2020-05-20T00:13:06Z")

</div>

Very cool examples, thank you! I’m quite happy to give up on the list if it leads to a better result. I think this composition approach should be functionally equivalent to the phantom builder pattern but the implementation looks a bit simpler here.

Both your composition approach and the phantom builder pattern have pretty good ergonomics - it’s possible to extract a bunch of attributes into a function for reuse, and combining attributes is actually somewhat easier than with a list. Plus of course there’s a more extensive constraint system here!

It looks like I can also have an attribute which is required by some functions but optionally accepted by others. Here I made `rounded` required by `square` and optionally accepted by `rect`: [https://ellie-app.com/8TRXW56gFdKa1](https://ellie-app.com/8TRXW56gFdKa1)

I’m not sure that the `Supported` tag is required, maybe `Set` is enough as optional attributes can use it as well?

I think the only potential limitation that I see is that required attributes are overconstrained: it isn’t possible to override required attributes once they are set, whereas I can do that with optional attributes. This might be inconvenient in practice: taking your second example, I might set paddings with `all` by default, but then want to override just the `top` padding for some elements. On the other hand, overriding can be confusing as well if it’s done by accident (but then, it would be consistent to disallow it for optional attributes too).

---

<div class="post-metadata">

**Author:** ![dmy](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dmy/32/702_2.png) [@dmy](https://discourse.elm-lang.org/u/dmy)\
**Post date:** [May 20, 2020, 12:35am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/5 "2020-05-20T00:35:57Z")

</div>

> [@alexkorban](#):
>
> I’m not sure that the `Supported` tag is required, maybe `Set` is enough as optional attributes can use it as well?

I used another tag to improve the signatures and error messages. I think it’s also somewhat deceiving to use `Set` on an optional argument, but it could work indeed.

> [@alexkorban](#):
>
> I think the only potential limitation that I see is that required attributes are overconstrained: it isn’t possible to override required attributes once they are set,

It’s possible. For example change the `top` signature to:

```elm
top : Int -> Edges { edges | top : a } -> Edges { edges | top : Set }
top n (Edges edges) =
    Edges { edges | top = n }

```

And the `top` edge will have to be set at least once, but can be overridden:  
[https://ellie-app.com/8TSkWszCsZga1](https://ellie-app.com/8TSkWszCsZga1)

```elm
padding (horizontal 5 >> top 10 >> bottom 20 >> top 9)

```

On the other hand, preventing optional arguments to be overridden may be trickier.

* * *

As an aside you should also take into account the types legibility and the compiler error messages when comparing solutions. For example using simple types like `elm-ui` for `padding`, `paddingXY` and `paddingEach` has the advantage to provide very simple signatures and error messages, even if it’s slightly less expressive. If I remember correctly, @mdgriffith even avoids record aliases in some functions types to improve error messages.

---

<div class="post-metadata">

**Author:** ![alexkorban](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/alexkorban/32/3242_2.png) [@alexkorban](https://discourse.elm-lang.org/u/alexkorban)\
**Post date:** [May 20, 2020, 12:53am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/6 "2020-05-20T00:53:54Z")

</div>

This is awesome!

You’re right about considerations for types. Things can get rather unwieldy if there is a large number of attributes, for example. There are also implications for testing and package publishing (due to type signature changes).

---

<div class="post-metadata">

**Author:** ![dmy](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dmy/32/702_2.png) [@dmy](https://discourse.elm-lang.org/u/dmy)\
**Post date:** [May 20, 2020, 1:01am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/7 "2020-05-20T01:01:30Z")

</div>

Where there are lots of attributes, extensible record aliases can help for phantom records. For example see the following PR that removes around 3500 lines of types:

> <https://github.com/rtfeldman/elm-css/pull/453>

Compiler errors are slightly impacted though, as noted there.

---

<div class="post-metadata">

**Author:** ![alexkorban](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/alexkorban/32/3242_2.png) [@alexkorban](https://discourse.elm-lang.org/u/alexkorban)\
**Post date:** [May 20, 2020, 1:14am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/8 "2020-05-20T01:14:11Z")

</div>

Yep, I tried aliases too but wasn’t sure about the legibility of errors afterwards.

---

<div class="post-metadata">

**Author:** ![dmy](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dmy/32/702_2.png) [@dmy](https://discourse.elm-lang.org/u/dmy)\
**Post date:** [May 20, 2020, 7:49am UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/9 "2020-05-20T07:49:32Z")

</div>

For completeness, note that you can also use the famous [`OneMoreThan`](https://github.com/rtfeldman/vector/blob/a3452ff6aa1c693c4550f2e5ff8eda0470b9ff9a/src/Vector.elm#L12) type to have attributes that you have to use a fixed number of times.

For example the ubiquitous “Good/Fast/Cheap Pick two”, guaranteed by types:  
[https://ellie-app.com/8V2wj6DBpnva1](https://ellie-app.com/8V2wj6DBpnva1)

```elm
module Main exposing (main)

import Html exposing (Html)

main : Html msg
main =
    Html.text <|
        Debug.toString <|
            pick (good >> fast)

type Unset
    = Unset Never

type Set
    = Set Never

type OneMoreThan a
    = OneMoreThan (OneMoreThan a)

type alias TwoMoreThan a =
    OneMoreThan (OneMoreThan a)

type Pick a
    = Pick { fast : Bool, good : Bool, cheap : Bool }

pick :
    (Pick { unset | picked : Unset } -> Pick { set | picked : TwoMoreThan Unset })
    -> Pick { set | picked : TwoMoreThan Unset }
pick toPick =
    toPick (Pick { fast = False, good = False, cheap = False })

fast : Pick { any | picked : a, fast : Unset } -> Pick { any | picked : OneMoreThan a, fast : Set }
fast (Pick soFar) =
    Pick { soFar | fast = True }

good : Pick { any | picked : a, good : Unset } -> Pick { any | picked : OneMoreThan a, good : Set }
good (Pick soFar) =
    Pick { soFar | good = True }

cheap : Pick { any | picked : a, cheap : Unset } -> Pick { any | picked : OneMoreThan a, cheap : Set }
cheap (Pick soFar) =
    Pick { soFar | cheap = True }

```

---

<div class="post-metadata">

**Author:** ![mdgriffith](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/mdgriffith/32/61_2.png) [@mdgriffith](https://discourse.elm-lang.org/u/mdgriffith)\
**Post date:** [May 20, 2020, 3:06pm UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/10 "2020-05-20T15:06:37Z")

</div>

Yup! I do avoid record aliases in those cases specifically for the error messages.

Basically to address this issue: [https://github.com/elm/error-message-catalog/issues/331](https://github.com/elm/error-message-catalog/issues/331)

---

<div class="post-metadata">

**Author:** ![system](https://canada1.discourse-cdn.com/flex035/uploads/elm_lang/original/1X/50a05e53677a2c3b47776d7abd0f113eb50193a1.png) [@system](https://discourse.elm-lang.org/u/system)\
**Post date:** [May 30, 2020, 3:06pm UTC](https://discourse.elm-lang.org/t/compile-time-checks-on-values-in-a-list-passed-as-an-argument/5796/11 "2020-05-30T15:06:42Z")

</div>

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