At TripShot, we love Haskell. We especially love its powerful type system that allows you to model things exactly the way you intend. In this article, we’ll explore a common problem when modeling domain entities, look into some potential solutions and finally present the solution which we have implemented at TripShot.
At TripShot, we work with vehicles a lot, so let’s take a model of a Vehicle as an example.
1-- Other types omitted for brevity
2
3dataVehicle=Vehicle
4 { vehicleId ::VehicleId
5 , name ::VehicleName-- E.g. John's Van
6 , licensePlate ::LicensePlate
7 , make ::VehicleMake
8 , model ::VehicleModel
9 , vin ::VIN
10 , year ::Year
11 , weight ::Lbs
12 }
Here we have a Vehicle with a bunch of vehicle-related fields. Nothing unlike most domain entities out there.
Now imagine someone made a typo in the vehicle name and we need to update it. We can’t do that now, so let’s write a function that updates a Vehicle. While we’re at it, we’ll also handle updates for more than the name field - mistakes happen, and we need to be able to correct them.
1dataVehicleUpdate=VehicleUpdate
2 { name ::VehicleName,
3 licensePlate ::LicensePlate,
4 make ::VehicleMake,
5 model ::VehicleModel,
6 vin ::VIN,
7 year ::Year
8 }
9
10updateVehicle ::VehicleUpdate->Vehicle->IOVehicle
11updateVehicle VehicleUpdate {..} vehicle =do
12 weight <- getWeightFromExternalService make model year
13
14return$
15 vehicle
16 { name = name,
17 licensePlate = licensePlate,
18 make = make,
19 model = model,
20 vin = vin,
21 year = year,
22 weight = weight
23 }
Simple enough. You’ll notice two things:
While this works fine, we then need to duplicate every field besides the ones we want to be able to update in both VehicleUpdate and Vehicle. This can quickly get tedious when you introduce more operations such as creating a Vehicle.
1dataVehicleCreate=VehicleCreate
2 { name ::VehicleName,
3 licensePlate ::LicensePlate,
4 make ::VehicleMake,
5 model ::VehicleModel,
6 vin ::VIN,
7 year ::Year
8 }
9
10dataVehicleUpdate=VehicleUpdate
11 { name ::VehicleName,
12 licensePlate ::LicensePlate,
13 make ::VehicleMake,
14 model ::VehicleModel,
15 vin ::VIN,
16 year ::Year
17 }
18
19dataVehicle=Vehicle
20 { vehicleId ::VehicleId,
21 name ::VehicleName,
22 licensePlate ::LicensePlate,
23 make ::VehicleMake,
24 model ::VehicleModel,
25 vin ::VIN,
26 year ::Year,
27 weight ::Lbs
28 }
In the real world, you’re likely to have a lot more fields. This can result in thousands of lines of duplication across domain models. Fortunately, Haskell offers a few mechanisms that can help us drastically reduce that.
One way to remedy this situation is to use the GADTs language extension. It would look something like this:
1data VehicleData = VehicleData
2 { name :: VehicleName
3 , licensePlate :: LicensePlate
4 , make :: VehicleMake
5 , model :: VehicleModel
6 , vin :: VIN
7 , year :: Year
8 , weight :: Lbs
9 }
10
11data Form = Create | Update | Model
12
13data Vehicle (f :: Form) where
14 CreateVehicle :: VehicleData -> Vehicle 'Create
15 UpdateVehicle :: VehicleData -> Vehicle 'Update
16 Vehicle :: VehicleId -> VehicleData -> Lbs -> Vehicle 'Model
17
18updateVehicle :: Vehicle 'Update -> Vehicle 'Model -> IO (Vehicle 'Model)
19updateVehicle = ...
We introduce a Form type that describes the shape of the model - for creating, for updating or for storing.
This has some benefits - we can use the Form type to state exactly the type of Vehicle we expect, but it isn’t very convenient to use due to the following reasons:
1vehicleId ::Vehicle'Model->VehicleId
2vehicleId (Vehicleid _ _ _ _) =id
2. We lost the ability to derive Generic. If you’re a user of generic-lens or otherwise like deriving instances such as FromJSON that may be a significant hindrance.
Again, luckily for us, Haskell’s type system can help us overcome those challenges as well!
Using the same Form type we introduced in the GADTs example, we can define type families that determine what the type of a field should be during compile time. You can think of those type families as type-level functions.
1dataForm=Create|Update|Model
2
3-- This is nothing but a simple function using
4-- pattern matching, only it is at the type level
5typefamilyId (f ::Form) where
6-- We use the Unit type to signal "empty"
7Id'Create= ()
8Id'Update= ()
9Id'Model=VehicleId
10
11typefamilyWeight (f ::Form) where
12Weight'Create= ()
13Weight'Update= ()
14Weight'Model=Lbs
15
16dataVehicle (f ::Form) =Vehicle
17 { id ::Id f,
18 name ::VehicleName,
19 licensePlate ::LicensePlate,
20 make ::VehicleMake,
21 model ::VehicleModel,
22 vin ::VIN,
23 year ::Year,
24 weight ::Weight f
25 }
26
27updateVehicle ::Vehicle'Update->Vehicle'Model->IO (Vehicle'Model)
28updateVehicle =...
29
30createVehicle ::Vehicle'Create->IO (Vehicle'Model)
31createVehicle =...
This is significantly less verbose than the GADTs example and with almost no drawbacks. We can easily derive Generic and use it for lenses or deriving various instances.
Deriving is a tiny bit more verbose than if we didn’t have the f parameter:
1data Vehicle (f :: Form) = Vehicle
2 { ...
3 }
4 deriving (Generic)
5
6deriving instance (ToJSON (Id f), ToJSON (Weight f)) => ToJSON (Vehicle f)
7deriving instance (FromJSON (Id f), FromJSON (Weight f)) => FromJSON (Vehicle f)
8Or we can use generic-lens with OverloadedLabels and get proper type inference:
9*> :t vehicle
10vehicle :: Vehicle 'Model
11
12*> :t vehicleCreate
13vehicleCreate :: Vehicle 'Create
14
15*> :t (vehicleCreate ^. #id)
16(vehicleCreate ^. #id) :: ()
17
18*> :t (vehicle ^. #id)
19(vehicle ^. #id) :: Text
At TripShot, we’ve found TypeFamilies to be an incredibly effective tool, enabling us to design more precise, flexible and maintainable systems. If you haven’t yet, we strongly encourage you to explore this approach. This use of TypeFamilies is also a great showcase of the power and flexibility of Haskell’s type system. We hope this inspires you to dive even deeper and discover more ways to build robust, well-designed systems.