https://images.prismic.io/tripshot-new/e2d2a57d-d982-4278-b21c-abd129088791_11-Variant-Forms-of-Entities-Using-Type-Families-f.png?ixlib=gatsbyFP&auto=compress%2Cformat&fit=max

Flexible, Correct Domain Models Using Type Families

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.

Identifying the problem

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:

  1. We don’t want to update the vehicleId field because that’s a unique identifier that we wish to preserve
  2. We don’t accept a weight field. Rather, we use the make, model and year to query an external service that will give us the weight.

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.

Approach 1: Use GADTs

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:

  1. We lost the built-in record fields and if we need those, we’ll need to write our own such as:
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!

Approach 2: Use TypeFamilies

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

Conclusion

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.