How to represent that a set of data is complete in a type-safe manner

Suppose that I am building a company website. I need to receive the information of the four branch offices, such as location, business hours, telephone number. My initial design would be like this:

type Branch
    = Lyon BranchInfo
    | Cordova BranchInfo
    | Sofia BranchInfo
    | Baku BranchInfo

type alias BranchInfo =
    { location: Location
    , businessHours : (Hour, Hour)
    , phoneNumber: PhoneNumber
    } 

The data should be coming in as a list in a JSON.

{ "branches" :
    [ { "name" : "Lyon", 
        "address" : "86 Rue Pasteur, 69007 Lyon, France", 
        "businessHours" : "09:00-16:00", 
        "phone" : "+33 4 78 69 70 00"  
      },
    ...
    ]
}                   

I could decode it into a list like:

branches : List Branch
branches = [ Lyon {...}, Cordova {...}, Sofia {...}, Baku {...} ] 

The JSON data should and will contain info for all of the branches. Unfortunately, the compiler cannot know that from the List type and will stoically give me a Maybe type whenever I try to fetch the data for a particular branch.

The question is, how can I convince the compiler that the decoded data contains 4 items and has complete set of data for all branches? What kind of collection type could convince compiler of such property?

Here’s my first attempt:

type alias Branches =
    { lyon : Branch
    , cordova : Branch
    , sofia : Branch
    , baku : Branch
    }

Above type can be used to guarantee that the data for four branches are there. But it cannot prevent accidental mixup of Branch instance.

Then here’s the second attempt

-- Branch.elm
type Branch
    = Lyon
    | Cordova
    | Sofia
    | Baku

type alias Branches =
    { lyon : Branch.Lyon.LyonBranch
    , cordova : Branch.Lyon.CordovaBranch
    , sofia : Branch.Lyon.SofiaBranch
    , baku : Branch.Lyon.BakuBranch
    }

-- Branch.Lyon.elm
type LyonBranch = LyonBranch BranchInfo

getBranchInfo ( LyonBranch branchInfo) = branchInfo

-- Branch.Cordova.elm
type CordovaBranch = CordovaBranch BranchInfo

-- Branch.Sofia.elm
type SofiaBranch = SofiaBranch BranchInfo

-- Branch.Baku.elm
type BakuBranch = BakuBranch BranchInfo

-- View.elm
view : Model -> Html msg
view model =
    div [] [ List.map (viewBranch model.branches) [ Lyon, Cordova, Baku, Sofia] ] 
 
viewBranch : Branches -> Branch  -> Html msg
viewBranch branches branch  =
    case branch of
        Lyon -> branches |> .lyon >> Branch.Lyon.getBranchInfo >> viewBranchInfo
...

This ensures that there are four branches, and each branch is annotated as a distinct type. It still cannot prevent mixup of BranchInfo data, but I think that’s unavoidable (I’d love to be proven wrong about this!).

It’s a bit more verbose than I would like and the names are repetitive and dull. Moreover, I have to use Branches record as a sort of database and use Branch type like a cumbersome key to retrieve data. I would prefer the Branch type to contain all the information about itself.

Or I could use Dict instead of record. I could decode the JSON into a Dict and let the decoding succeed if it has all four keys. Unfortunately, the keys would have to be hardcoded since it’s impossible to iterate over constructors in Elm.

-- Branch.elm
type Branch
    = Lyon
    | Cordova
    | Sofia
    | Baku

type Branches = Branches (Dict String BranchInfo)

-- public
getInfo : Branch -> Branches -> BranchInfo
getInfo branch branches = map (Dict.get (toString branch)) branches |> Maybe.withDefault  defaultBranchInfo

-- private
map f (Branches dict) = f dict

-- View.elm
viewBranch : Branches -> Branch -> Html msg
viewBranch branches branch =
    case branch of
        LyonBranch -> getLyon 
...

This is much shorter, and although I make sure that the data has valid properties when decoding JSON, I still feel like I am cheating the type system by using Maybe.withDefault like this.

Overall, I’m not fond of either approach. If someone could show me a better way to do this in Elm, I’d really appreciate!

1 Like

This really depends a lot on how you are going to use the data.

Another option is to use phantom types to prevent the mixup:

type Branch location = Branch { ... } 

type Lyon = Lyon
type Cordova = Cordova
type Sofia = Sofia
type Baku = Baku

type alias Branches =
    { lyon : Branch Lyon
    , cordova : Branch Cordova
    , sofia : Branch Sofia
    , baku : Branch Baku
    }

But ultimately I’d advice to just proceed with an option, write the code and see where it’s problematic, then refactor. There are no silver bullets here I think (type system is too limited), so the best you can do is pick the option that is best for your specific use case and usage pattern.

Sounds to me like you are looking for a custom type with 4 parameters.

type AllBranches =
   AllBranches BranchData BranchData BranchData BranchData

The type system can see that it always has 4 elements, so you won’t get any Maybes.
Extract the contents using pattern matching or destructuring.

This is similar to the ‘first attempt’:

type alias Branches =
    { lyon : Branch
    , cordova : Branch
    , sofia : Branch
    , baku : Branch
    }

which I think is clearer in this case, because there the four branches can be referred to by name as well as by position.

Both share the disadvantage that you will not be able to prevent accidental mixup of a Branch instance.

Phantom type looks promising here! I’ll give it a go :smiley:

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