As a Haskell programming expert, I have encountered various architectural decisions in my past Haskell projects. One of such instances required a choice between two different approaches to represent and manipulate data: algebraic data types (ADTs) versus extensible records.
We were working on a library that serves as an abstraction for different event stores. The primary function of this library was to facilitate the storage and retrieval of events emitted by different parts of an application. This required a highly modular and extensible design, as the library had to support various event sources and data stores.
We had two main alternatives:
1. Algebraic Data Types (ADTs): This is the traditional approach used by Haskellers to model and manipulate data, which involve combining products and sums types. This allows for the easy creation of types with multiple constructors, each representing a variant of the data type.
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics (Generic)
import Data.Aeson (ToJSON, FromJSON)
data Event = Click { xPos :: Int, yPos :: Int }
| Impression { pageUrl :: String }
| Conversion { userId :: String, value :: Double }
deriving (Show, Generic, ToJSON, FromJSON)
2. Extensible Records: An alternative approach to using ADTs is to use extensible records, a feature provided by various Haskell libraries (such as Vinyl, Frames, or generic-lens). Extensible records enable the representation of data structures as a collection of fields with an associated type, where the composition of the fields can be dynamic.
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TypeOperators #-}
import Data.Vinyl
import GHC.TypeLits
import Data.Type.Equality
type WriteOperation = "write" ::: Bool
type StoreType = "storeType" ::: String
-- Type synonym for the generic event representation
type EventRecord = [WriteOperation, StoreType]
We had to carefully weigh the pros and cons of each approach:
- ADTs provide type safety and allow for pattern matching, while extensible records offer a more dynamic and flexible way of dealing with data.
- ADTs are easier to understand for most developers and follow well-known patterns, while extensible records might be less familiar and harder to grasp for newcomers.
- Extensible records provide greater extensibility to library users, enabling them to easily add and manipulate custom fields in the event data, whereas ADTs might require non-trivial changes in the library for such extensions.
After considering these factors, we chose to use extensible records for our project:
1. The extensible nature of records allowed our library users to add custom fields to the events easily, enabling better adoption.
2. We decided to provide an extensive set of examples and thorough documentation to make the usage of extensible records more understandable for developers new to our library.
3. We concluded that the trade-off in complexity was worth the increased flexibility offered by extensible records.
As a result, our library provided a flexible foundation on which users could build custom event stores and sources tailored to their specific needs. The choice of extensible records allowed for seamless integration and extension of the library in various application domains. Overall, the project’s outcome was successful, as it fulfilled the initial requirements and goals.