domain-0.1.1.5: Codegen helping you define domain models
Safe HaskellSafe-Inferred
LanguageHaskell2010

Domain.Docs

Synopsis

    How it works

    "domain" operates around Schema AST which describes the structure of your model. This AST gets constructed by either parsing a file or a quasi-quote conforming to a further described format. Then it is used to generate Haskell type declarations and typeclass instances according to your configuration. All that is done at compile time, so you're incurring zero run time cost for using "domain".

    Schema Syntax Reference

    Schema definition is a YAML document listing declarations of your domain types. The listing is represented as a dictionary from type names to their definitions. There is 3 types of definitions: Product, Sum, Enum.

    Product

    Defines a type comprised of other types using Product type composition, associating a unique textual label with each member. You may know it as "record".

    Here's an example of a product type declaration in schema:

    NetworkAddress:
      product:
        protocol: TransportProtocol
        host: Host
        port: Word16

    Depending on the settings you provide one of the following Haskell type declarations can be generated from it:

    data NetworkAddress =
      NetworkAddress !TransportProtocol !Host !Word16
    data NetworkAddress =
      NetworkAddress {
        networkAddressProtocol :: !TransportProtocol,
        networkAddressHost :: !Host,
        networkAddressPort :: !Word16
      }
    data NetworkAddress =
      NetworkAddress {
        _protocol :: !TransportProtocol,
        _host :: !Host,
        _port :: !Word16
      }
    data NetworkAddress =
      NetworkAddress {
        protocol :: !TransportProtocol,
        host :: !Host,
        port :: !Word16
      }

    Accessing fields

    Regardless of the way you choose to generate the data declaration, neat mechanisms of accessing members can be provided using the automatically generated IsLabel instances or instances of LabelOptic (using the "domain-optics" package).

    E.g., here's how you can be accessing the members of the example data-type:

    getNetworkAddressPort :: NetworkAddress -> Word16
    getNetworkAddressPort = #port
    mapNetworkAddressHost :: (Host -> Host) -> NetworkAddress -> NetworkAddress
    mapNetworkAddressHost = over #host -- Using "domain-optics" and "optics"

    Sum

    Defines a type comprised of other types using Sum type composition, associating a unique textual label with each member. You may know it as tagged union or variant.

    Here's an example of a schema declaration of a sum type:

    Host:
      sum:
        ip: Ip
        name: Text

    The following Haskell code will be generated from it:

    data Host =
      IpHost !Ip |
      NameHost !Text

    As you can see the constructor names are intentionally made to be unambiguous. You may already be thinking "But the code is gonna get so verbose". It's not. Thanks to the automatically generatable IsLabel and LabelOptic instances.

    E.g., here's how you'll be able to access the variants of the data-type:

    getHostIp :: Host -> Maybe Ip
    getHostIp = #ip
    ipHost :: Ip -> Host
    ipHost = #ip
    mapHostIp :: (Ip -> Ip) -> Host -> Host
    mapHostIp = over #ip -- Using "domain-optics" and "optics"

    Multi-member sums

    It is possible to provide multiple members of a sum variant using a comma-separated list or YAML sequence. You can provide zero members as well. E.g.,

    Error:
      sum:
        channel:
          - ChannelId
          - Text
        connectionLost:

    This will generate the following declaration:

    data Error =
      ChannelError !ChannelId !Text |
      ConnectionLostError

    Depending on the number of variant members the generated accessors will point to tuples or booleans:

    getErrorChannel :: Error -> Maybe (ChannelId, Text)
    getErrorChannel = #channel
    
    getErrorConnectionLost :: Error -> Bool
    getErrorConnectionLost = #connectionLost

    Enum

    Type which can have one value out of a specific set of options.

    Here's an example of a schema declaration of an enum type:

    TransportProtocol:
      enum:
        - tcp
        - udp

    This will generate the following Haskell data type:

    data TransportProtocol =
      TcpTransportProtocol |
      UdpTransportProtocol

    The following IsLabel helpers will be available for it:

    tcpTransportProtocol :: TransportProtocol
    tcpTransportProtocol = #tcp
    
    getTransportProtocolTcp :: TransportProtocol -> Bool
    getTransportProtocolTcp = #tcp

    Notes

    List Data-type

    Since square brackets get interpreted in YAML as array literal, you have to explicitly state that the value is a string literal. To achieve that prefix the value with the vertical line character (|). E.g.,

    Artist:
      product:
        name: Text
        genres: | [Genre]

    Reserved Names

    You can use the otherwise banned field names like "data", "type", "class".

    Newtypes

    Single-field products get represented as newtypes, so use them whenever you need to generate a newtype declaration.

    Type Aliases

    Schemas intentionally lack support for type aliases, since they haven't yet proven to be very useful in practice.

    However we're open for discussion on the subject. So do provide your arguments on the project's issue tracker if you feel like they should be added as a feature.

    Polymorphic Types

    Polymorphic types are not supported. Domain model is expected to consist of specific data structures, not abstractions.

    Instance Derivation

    Instance derivation is intentionally isolated from the schema definition to let both tasks be focused. Instances get derived for all the types in your schema that they are suitable for. We treat schema as a group entity over multiple types having them share settings including the instance generation rules.

    Whenever you find yourself in a situation where you need different instances for parts of your model it should serve as a signal that you're likely dealing with multiple models merged into one. The solution to such situation is to extract smaller models. When dealing with Domain Schema that is what will also let you generate different instances.

    Custom Derivers

    The "domain" package does not expose any means to create custom derivers, since its API focuses on their usage as part of the problems of the general audience. To create custom derivers you'll have to use the "domain-core" package, which exposes the internal definition of the Deriver abstraction and everything you need to define custom derivers.

    Such isolation of libraries lets us have a stable API for the general audience, serving for better backward compatibility, and keep it isolated from the distractions of lower level details.

    Deriver Extensions

    We expect the community to publish their general custom derivers as extensional packages.

    So far the following packages are available:

    If you're looking to contribute, some likely needed candidates for extensions are "QuickCheck", "binary".