
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.
-- Other types omitted for brevity
dataVehicle=Vehicle
{ vehicleId ::VehicleId
, name ::VehicleName-- E.g. John's Van
, licensePlate ::LicensePlate
, make ::VehicleMake
, model ::VehicleModel
, vin ::VIN
, year ::Year
, weight ::Lbs
}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.
dataVehicleUpdate=VehicleUpdate
{ name ::VehicleName,
licensePlate ::LicensePlate,
make ::VehicleMake,
model ::VehicleModel,
vin ::VIN,
year ::Year
}
updateVehicle ::VehicleUpdate->Vehicle->IOVehicle
updateVehicle VehicleUpdate {..} vehicle =do
weight <- getWeightFromExternalService make model year
return$
vehicle
{ name = name,
licensePlate = licensePlate,
make = make,
model = model,
vin = vin,
year = year,
weight = weight
}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.
dataVehicleCreate=VehicleCreate
{ name ::VehicleName,
licensePlate ::LicensePlate,
make ::VehicleMake,
model ::VehicleModel,
vin ::VIN,
year ::Year
}
dataVehicleUpdate=VehicleUpdate
{ name ::VehicleName,
licensePlate ::LicensePlate,
make ::VehicleMake,
model ::VehicleModel,
vin ::VIN,
year ::Year
}
dataVehicle=Vehicle
{ vehicleId ::VehicleId,
name ::VehicleName,
licensePlate ::LicensePlate,
make ::VehicleMake,
model ::VehicleModel,
vin ::VIN,
year ::Year,
weight ::Lbs
}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:
data VehicleData = VehicleData
{ name :: VehicleName
, licensePlate :: LicensePlate
, make :: VehicleMake
, model :: VehicleModel
, vin :: VIN
, year :: Year
, weight :: Lbs
}
data Form = Create | Update | Model
data Vehicle (f :: Form) where
CreateVehicle :: VehicleData -> Vehicle 'Create
UpdateVehicle :: VehicleData -> Vehicle 'Update
Vehicle :: VehicleId -> VehicleData -> Lbs -> Vehicle 'Model
updateVehicle :: Vehicle 'Update -> Vehicle 'Model -> IO (Vehicle 'Model)
updateVehicle = ...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:
vehicleId ::Vehicle'Model->VehicleId
vehicleId (Vehicleid _ _ _ _) =id2. 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.
dataForm=Create|Update|Model
-- This is nothing but a simple function using
-- pattern matching, only it is at the type level
typefamilyId (f ::Form) where
-- We use the Unit type to signal "empty"
Id'Create= ()
Id'Update= ()
Id'Model=VehicleId
typefamilyWeight (f ::Form) where
Weight'Create= ()
Weight'Update= ()
Weight'Model=Lbs
dataVehicle (f ::Form) =Vehicle
{ id ::Id f,
name ::VehicleName,
licensePlate ::LicensePlate,
make ::VehicleMake,
model ::VehicleModel,
vin ::VIN,
year ::Year,
weight ::Weight f
}
updateVehicle ::Vehicle'Update->Vehicle'Model->IO (Vehicle'Model)
updateVehicle =...
createVehicle ::Vehicle'Create->IO (Vehicle'Model)
createVehicle =...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:
data Vehicle (f :: Form) = Vehicle
{ ...
}
deriving (Generic)
deriving instance (ToJSON (Id f), ToJSON (Weight f)) => ToJSON (Vehicle f)
deriving instance (FromJSON (Id f), FromJSON (Weight f)) => FromJSON (Vehicle f)
Or we can use generic-lens with OverloadedLabels and get proper type inference:
*> :t vehicle
vehicle :: Vehicle 'Model
*> :t vehicleCreate
vehicleCreate :: Vehicle 'Create
*> :t (vehicleCreate ^. #id)
(vehicleCreate ^. #id) :: ()
*> :t (vehicle ^. #id)
(vehicle ^. #id) :: TextAt 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.