Skip to content

Instantly share code, notes, and snippets.

@jfmengels
Last active April 12, 2020 16:10
Show Gist options
  • Save jfmengels/d9ee1f288873d7ecad16d0cb8d415ce0 to your computer and use it in GitHub Desktop.
Save jfmengels/d9ee1f288873d7ecad16d0cb8d415ce0 to your computer and use it in GitHub Desktop.
API design puzzle - The car factory

Imagine you try to design "factories". You have factories that can build Diesel cars, and factories that can build any kind of car (Diesel and Gas, but other kinds of cars could exist). Let's say you want the user to be able to create their customized car factory, for which you provide an API, like

I have a module Factory which defines a Factory and a Car type, all opaque. I provide two functions createCarFactory and createDieselCarFactory, where the first one takes a use-provided function that can return a list of any cars (and they can be mixed), and the second one can only generate Diesel cars.

I want the garantee as the module author, that users won't be able to provide functions that create cars other than diesel cars in the createDieselCarFactory, while still allowing to create any kind of car with the createCarFactory function.

I want the type system to be the one to complain if these guarantees are not respected by the user.

For simplicity's sake, imagine that cars have no data.

The following is an example of the API could look like, but without the garantees I mentioned. You are allowed to tweak change it however you want.

module Factory exposing (Factory, createCarFactory, createDieselCarFactory, Car, gasCar, dieselCar)

type Car = DieselCar | GasCar
createDieselCarFactory : (data -> List Car) ->Factory
createCarFactory : (data -> List Car) -> Factory

gasCar = GasCar
dieselCar = DieselCar

This is how the API could be used:

import Factory

-- Ok. notice that there are different variants of the car in the resulting list
factory : Factory
factory = 
  Factory.createCarFactory (\_ -> [ Factory.dieselCar, Factory.gasCar ])

-- Also ok
factory : Factory
factory = 
  Factory.createDieselCarFactory (\_ -> [ Factory.dieselCar ])

-- NOT OK
factory : Factory
factory = 
  Factory.createDieselCarFactory (\_ -> [ Factory.dieselCar, Factory.gasCar ])

The solution I went with in elm-review, because I only care (by analogy) whether the car is a diesel or not (and not necessarily whether it's a gas or plutonium car) :

type Car fuel = DieselCar | GasCar | PlutoniumCar

diesel : Car { isDiesel : () }
diesel = DieselCar

gas : Car fuel
gas = GasCar

plutonium : Car fuel
plutonium = PlutoniumCar

createCarFactory : (data -> List (Car fuel)) -> Factory
createDieselCarFactory : (data -> List (Car { isDiesel : () })) -> Factory

Actually, my point in my situation was mostly to forbid diesel cars, but allow the rest, so I did

type Car fuel = DieselCar | GasCar | PlutoniumCar

diesel : Car {}
diesel = DieselCar

gas : Car fuel
gas = GasCar

plutonium : Car fuel
plutonium = PlutoniumCar

createCarFactory : (data -> List (Car fuel)) -> Factory
createNonDieselCarFactory : (data -> List (Car { notDiesel : () })) -> Factory

You can look at error and errorForModule in https://package.elm-lang.org/packages/jfmengels/elm-review/latest/Review-Rule

@7hoenix
Copy link

7hoenix commented Apr 12, 2020

This is awesome. Thanks for sharing :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment