DEV Community

Use opaque types in Elm!

Héctor Ramón on October 27, 2018

It's been a while since I joined the Elm Slack. During this time, I have seen different folks ask how, and when, to use opaque types. I have also s...
Collapse
 
kwitgit profile image
kwitgit

Great explanation of opaque types, thank you!

What do you think of the following additional improvement? The model currently specifies both title and body as String types. So in the Submit logic it's possible to accidentally pass the wrong string, such as Question.titleFromString model.body but then if body still happens to pass the validation for title your view will be wrong but give you no errors. To prevent this, create a type alias TitleString = String and then use that in the model instead of plain String. Then Bob will also know that TitleString and BodyString can never be interchanged accidentally. Does that seem like overkill (or have a flaw I'm not seeing)?

That's not really a question about opaque types, it's on the general theme of "catch everything at compile time" which is what's great about Elm!

Collapse
 
hecrj profile image
Héctor Ramón • Edited

Good question!

The issue is that a type alias is just a new name for a type (an alias). Therefore, TitleString is a just new way to say String. This means that Question.titleFromString model.body would still compile, even when using TitleString in your Model and Question API. The compiler would see String everywhere.

We can use an opaque type! Imagine we define a Question.TitleField module with this API:

module Question.TitleField exposing (TitleField, blank, title, view)

import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events
import Question

type TitleField
    = TitleField String

blank : TitleField
blank =
    TitleField ""

title : TitleField -> Result String Question.Title
title (TitleField value) =
    Question.titleFromString value

view : TitleField -> Html TitleField
view (TitleField value) =
    Html.input
        [ Events.onInput TitleField
        , Attributes.value value
        , Attributes.type_ "text"
        ]
        []

This module ties a a TitleField value with its form field view and its validation. Finally, we use TitleField in our Model, using blank to initialize it, title for validation, and view to render it!

composable-form can help you write this:

module Question.TitleField exposing (TitleField, blank, form)

import Form

type TitleField
    = TitleField String

blank : TitleField
blank =
    TitleField ""

form : Form TitleField Question.Title
form =
    Form.textField
        { parser = Question.titleFromString
        , value = \(TitleField value) -> value
        , update = \newValue oldValue -> TitleField newValue
        , attributes =
            { label = "Question title"
            , placeholder = "Type the statement of your question..."
            }
        }
Collapse
 
kwitgit profile image
kwitgit

I'm glad this led back to opaque types. 8-D Using something like TitleField type is probably too much for the simple example, but a great solution for a site that has to be really robust! hecrj/composable-form looks great, I'm going to check that out too.

As for type aliases... I don't know enough about compiler design to have an informed opinion, but what I described is how I would "want" type aliases to work. I totally understand that after being compiled they're all just Strings (in this example), but if the coder specifies a different name I think I want a step somewhere that will catch that if the wrong name is used. But your TitleField solution is better anyway, it provides a lot more than just a new name for a type. 8-)

Collapse
 
perty profile image
Per Lundholm

Great post. I was wondering about the use of Http.Error. Why are you using that when creating a Question, it has nothing to do with Http?

Collapse
 
hecrj profile image
Héctor Ramón • Edited

Thank you!

As I say here:

We will work with an example of what I think is a fairly common scenario: a form that validates and submits some data to create a new resource remotely.

We assume that the Question needs to be created remotely in some backend. Although I don't mention Http specifically, the most common strategy is to do this by making a request to an HTTP API.

In any case, the error type could be something entirely different. It is not relevant to understand the concepts presented in the post.

Collapse
 
perty profile image
Per Lundholm

Ok, get it. Thanks.

Collapse
 
steppeeagle profile image
SteppeEagle

How about to hide 'validation' behind the 'create' method?
What do you think about it?

Collapse
 
hecrj profile image
Héctor Ramón

Then the create function does multiple things. Not desirable. For instance, how would you show validation errors without submitting the form?

Collapse
 
steppeeagle profile image
SteppeEagle

Then the create function does multiple things

For instance we have function which get 2 numbers and return sum of them. The first number is odd and the second is even.

function oddEvenSumm(odd, even) {
  if (even % 2 == 1) {
    throw new Error('not even one')
  }
  if (odd % 2 == 0) {
    throw new Error('not odd one')
  }

  return odd + even;
}

Should we create new types for these values (like you did for title and body) because we validate them?
Do we break Single Responsobiliy Principal?
Does it make sence to create separate type if we don't use them in other places?

Thread Thread
 
hecrj profile image
Héctor Ramón • Edited

Opaque types allow to capture guarantees and propagate them. If you only need these guarantees on a specific part of your code and there is no need to propagate them, then it is probably not worth it to create opaque types for them. In other words, not every "validation check" should result in an opaque type.

However, most of the time this is an API design choice. For instance, I would understand this API better than the example you provided:

type Odd = Odd Int
type Even = Even Int

oddFromInt : Int -> Maybe Odd
evenFromInt : Int -> Maybe Even
sum : Odd -> Even -> Odd

The advantages here are:

  • We move potential errors closer to their cause (conversions).
  • We keep sum error-free! We can call it multiple times without having to deal with errors: sum (sum odd even) even
  • We specify that the returned sum value is guaranteed to be an Odd number.