Hello, building an application, I’ve been having a hard time coming to terms to a defined way to write data modules. I’ve watched pretty much every talk from Evan and Richard Feldman and some other Elm developers and still haven’t decided on the proper way to do this. Let me explain.
For every field in the Model
, which mirrors an entity from my application’s domain model, I create a module to represent and manipulate it. Let’s consider I’ve got a Person
entity in my domain and my Model
is meant to have a field in which a given collection (whatever data structure that may be) of Person
. Right now, I’d do it like this:
module Model.Person exposing (Person, Id, Collection, ....)
type Id = Id Int
type Collection = Collection (Dict Int Person)
type alias Metadata = { id : Id, name : String, age : Int }
type Person = Person Metadata
-- Field extraction
getId : Person -> Id
getId ( Person { id } ) =
id
getName : Person -> String
getName ( Person { name } ) =
name
getAge : Person -> Int
getAge ( Person { age } ) =
age
-- Collection operations
emptyCollection : Collection
emptyCollection =
Collection Dict.empty
collectionSize : Collection -> Int
collectionSize (Collection collection) =
Dict.size collection
collectionToList : Collection -> List Person
collectionToList (Collection collection) =
Dict.values collection
insert : Person -> Collection -> Collection
insert person (Collection collection) =
Collection <| Dict.insert (getId person) person collection
... other similar operations
-- Other functions
isOfAge : Person -> Bool
isOfAge person =
(getAge person) >= 18
... other similar functions
-- Decoders
idDecoder : Decode.Decoder Id
idDecoder =
Decode.int
|> Decode.andThen
(\int -> Decode.succeed (Id id))
personDecoder : Decode.Decoder Person
personDecoder =
Decode.succeed Metadata
|> Pipeline.required "id" idDecoder
|> Pipeline.required "name" Decode.string
|> Pipeline.required "age" Decode.int
-- Private functions
...
and the Model
would have something like this:
type alias Model = { person = Person.Collection }
I’ve found this to be a good pattern, which has the following advantages:
- The APIs are well defined and the user is cannot mess with the data.
- I like the
Collection
type quite a lot. It abstracts away which data structure is being used so we don’t need to pass them around function signatures, for example. Also gives good expressiveness since we readPerson.Collection
in the signatures which sounds nice to me.
However I’ve got a few questions:
- Can this kind of Opaque Types for
Person
where we have to access everything through an API be cumbersome for users using this module, given its meant to be an internal API? Meaning it’s not an external library offered to users. Inside the project a developer has to constantly write stuff like(getAge person)
instead ofperson.age
. - Are the
Collection
and the “other functions” inside the same module a bad violation of the Single Responsibility Principle? - I enjoy the
Collection
abstraction but the functions likeemptyCollection
andcollectionSize
feel weird and poorly designed from an API standpoint. They stick out like something is wrong in the design, but I don’t know how I could make it better.