Opaque types in elm-land

Hello everybody

As an elm beginner I have been busy listening to my favourite podcast: elm-radio.

As they keep talking about opaque types I thought I would like to try it out.

I am also very excited about the new elm spa framework elm-land by Ryan Haskell-Glatz. So I wanted to use an opaque type following the elm.land implementation of a simple pokemon website.
Here you find my version of the site: https://willowy-twilight-ed9bec.netlify.app/ and (here was the link to the official documentation but I am only allowed to post two links in my first post :()

The elm-land documentation suggest implementing a type “Data” for dealing with http requests:

type Data value
    = Loading
    | Success value
    | Failure Http.Error

the init function of the page Home_.elm calls the function getFirst150 (where I get the first 150 pokemon). As this function is tied to the type “Pokemon”, I implemented this type in a separate Pokemon.elm file, like so:

type alias PokeRecord =
    { name : String
    , order : Int
    , picture : String
    , types : List String
    }


type Pokemon
    = Pokemon PokeRecord

This way, the constructor of the type Pokemon is not exposed to other modules. So if you need to implement a new feature or if you have any bugs or problems you only need to check Pokemon.elm. Opaque types for me feel a little bit like classes in object oriented programming. But as I don’t really understand object oriented programming, I am not an expert on this topic!

All my Pokemon helper functions look something like this:

getPokemonName : Pokemon -> String
getPokemonName pokemon =
    case pokemon of
        Pokemon pokeRecord ->
            pokeRecord.name

hopefully this is correct!

Ok, now my getFirst150 function in the Pokemon.elm file looks like this:

getFirst150 : { onResponse : Result Http.Error (List Pokemon) -> msg } -> Cmd msg
getFirst150 options =
    Http.get
        { url = "https://pokeapi.co/api/v2/pokemon?limit=150"
        , expect = Http.expectJson options.onResponse pokeApiDecoder
        }

this function, along with some other helper functions and the Pokemon type, is exposed to my Home_.elm file.

my model in the Home_.elm file looks like this:

type alias Model =
    { pokemonData : Api.Data (List Pokemon)
    , pokemon : List Pokemon
    }

my goal is to handle the http side with pokemonData which is of type Data, that is either “Loading”, “Successful” with the List of Pokemon or “Failure” with the error. Only in the success case do I want to assign the listOfPokemon to my model, like so:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PokeApiResponded (Ok listOfPokemon) ->
            ( { model | pokemonData = Api.Success [], pokemon = listOfPokemon }
            , Cmd.none
            )

        PokeApiResponded (Err httpError) ->
            ( { model | pokemonData = Api.Failure httpError }, Cmd.none )

now I can implement more and more functionality for my pokemon type.

Elm-land is very cool to work with and thanks to the documentation it is very easy to publish the site.

The code of the implementation is available on: GitHub - alpakaxaxa/elm-pokemon-api: Interact with pokemon api using elm-land framework
Any feedback regarding the implementation of the opaque type would be much appreciated! It is the first time that I use elm-land and an opaque type.

Thanks so much to Dillon Kearns and Jeroen Engels for teaching me a lot about programming and thanks to Ryan Haskell-Glatz for building an amazing spa framework!

2 Likes

There is one thing that can speed up your work with single-member unions. Instead of using case-of, you can destructure them like this:

getPokemonName : Pokemon -> String
getPokemonName (Pokemon pokemon) =
    pokemon.name

Further, the record itself can also be destructured

getPokemonName : Pokemon -> String
getPokemonName (Pokemon {name}) =
    name
1 Like

As for the model itself, depending on the use case, you don’t need to separate the response and its success data into 2 different fields. This would be enough:

type alias Model =
    { pokemonData : Api.Data (List Pokemon)
    }

This way you have a safety net and must handle all states when you want to manipulate the pokemon list.

1 Like

Thank you so much! It was intentional choice, atleast :sweat_smile:
Let’s say I want my Pokemon type to have different types: E.g. type Pokemon = RegularPokemon pokeRecord | FavouritePokemon pokeRecord
Maybe the example sucks, but for an application that I am building my opaque type could have a lot of different types. So, I thought I don’t want to also mix in the api related stuff like Loading/Succesful/Failure (to the Pokemon type). So I decided to have separate fields and only create a PokekomList in case the http request has been successful.
But I don’t know if this approach makes sense :innocent:

have separate fields and only create a PokekomList in case the http request has been successful

If the request is successful then you already have the list and don’t need to get it from somewhere else. For example in your code here you can do

Api.Success pokemon ->
    viewPokemonList pokemon

If you want a situation where when you’re loading new Pokemon you still display the previous list then you could change your Data type to something like

type Data value
    = Loading
    | LoadingWithPrevious value
    | Success value
    | Failure Http.Error

where LoadingWithPrevious hold data from the previous Success but is displayed similar to Loading, or however you want to handle it.

2 Likes

I see, thank you so much.
I am building some kind of interactive “game” in elm. It is still about displaying pure html but it is a lot more dynamic than a normal website/spa.

That means, in the beginning I load data from an api.
If it is not successful, I display somewhat like a try again button and a fat error message.
When the data is loaded correctly, the “game” is generated and starts. Afterwards I am constantly modifying the “PokemonList” until the game is over and I submit the result to the server.

I thought it would be unnecessary to have my “PokemonList” wrapped with Api.Data as I don’t want to constantly deal with cases that are not relevant/happening anyway (like I am in the middle of the game and I have to check if the data loaded correctly).

As I want to implement opaque types in my “game” I decided to try it out in a pokemon site first. However, now I understand that my example and asking for advice without giving adequate/real context was stupid, sorry for that, it was not intentional.

While typing this reply it really clicked for me. I was a bit worried about my model choice being stupid and now after playing with an opaque type and talking to you guys I feel a lot more confident in my approach.

Thank you guys so much for your support, the elm community has been the most supportive for me by very far.

3 Likes

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