JSON Decode to Opaque Type

Hi Joe,

First of all, it’s not quite what I would call an Opaque type. An opaque type is one in which you cannot see the details. You achieve this in Elm via not exposing the constructors of the opaque type:

module SomeType exposing (SomeType) -- Note, not SomeType(..)

type SomeType =
    SomeType { value1 : String, value2 : Int }

Because of this, you would have to provide someway to create and use values within that module. The reason for doing this would be because there is some way to create an invalid value that you want to make impossible, so for example you might do something like this:

module Length exposing
    ( Length
    , fromInt
    , toInt
    )

type Length = Length Int

fromInt : Int -> Length
fromInt i =
    -- So it is impossible to create a negative length.
    Length (abs i)

toInt : Length -> Int
toInt l =
    case l of
         Length i ->
              i

Because it is impossible to create a value of type Length from outside of this module, we know that the only way to create a Length is by using Length.fromInt and therefore we know that all Lengths in this program will be non-negative. That’s what an opaque type is for me, but do note that there isn’t always universal agreement on typing terms.

The SomeType in your program, may or may not be oqaque from that definition, but even if is, you would need some way to create values of that type anyway. In addition, you would likely keep the definition of the decoder in the same module, so as far as that was concerned it wouldn’t be opaque. The SomeType in your program is what is generally called in Elm-land as a Custom type, or a Custom tagged type. What we mean is ‘one of those types that has a set of constructors in its type definition’.

Now, your actual question. As was suggested, if your SomeType was a type alias to a record type you could do the following:

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  Decode.map2 SomeType
    (Decode.field "value1" Decode.string)
    (Decode.field "value2" Decode.int)

You only need to tweak this a little bit in your case. The key point is that the first argument to Decode.map2 requires the type (a -> b -> c), which in this case is instantiated to String -> Int -> SomeType. You can give any function with that type, but the most obvious one would be:

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  let
       createSomeType value1 value2 =
           SomeType { value1 = value1, value2 = value2 }
  in
  Decode.map2 createSomeType
    (Decode.field "value1" Decode.string)
    (Decode.field "value2" Decode.int)

You can do the analogous with the Pipeline version.

3 Likes