Skip to content

Instantly share code, notes, and snippets.

@dfithian
Last active January 30, 2018 18:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dfithian/84263a05bef37595955834449d224061 to your computer and use it in GitHub Desktop.
Save dfithian/84263a05bef37595955834449d224061 to your computer and use it in GitHub Desktop.

Title

Haskell DJs: Compose a hit with Vinyl

Session Topics

  • Languages
  • Concepts
  • Libraries

Session Type

  • Educational Session (50 minutes)
  • Could become Hop Workshop (120 minutes) by adding Swagger documentation to an API

Abstract

PureScript has row types, and it's widely considered to be an excellent feature. Why can't we have the same thing in Haskell? With Vinyl, and a layer of sugar provided by Composite Records, we can! In this talk, I will introduce row types in Haskell as implemented by the Vinyl library, discuss their utility for API design, and demonstrate how to leverage them to define a JSON spec.

Row types emphasize subsetting as a way of testing equality, downcasting, and translation of types by deleting/injecting fields. These features motivate us to want to use row types throughout our entire program instead of just the internals. However, nobody has written a library to do this until just last year. Composite Records, written in 2017 and inspired by the Frames library, uses row types with static identifiers and recursive typeclasses solve a host of problems related to schema definition, from JSON interchange to database design. I will show the approach Composite Records takes towards JSON schema definition and provide resources to investigate the rest, including Opaleye models and Swagger code generation.

Familiarity with, but not fluency in, both Lens and Aeson, especially Aeson Better Errors, will be expected for this talk.

Content Relevancy

Row types bring a host of great features. Using them in Haskell is possible, but not at the fringes of a program. This educational session will introduce a set of libraries that help alleviate that pain. The attendee will come away with an understanding of how to use these libraries in a modern Haskell application.

Notes

At a high level, I want to talk about the benefits of using row types in general, how they are used in Haskell, and finally how to define a JSON spec with them. This discussion will encompass a few different libraries. Familiarity, but not fluency, with both Lens and Aeson, especially Aeson Better Errors, are prerequisites.

My talk will start with the abstract. Why are row types useful? For one, you can compare equality based on subsets of fields within a given type. Second, once subsetting is supported, you can do downcasting to a different type, or translation of types by deleting/injecting fields (as two examples). This should take less than 5 minutes and a single slide. It's just an introduction, no implementation details of any kind. That said there will be time needed for people to settle and to answer questions about how to find the code on GitHub or find the slides on slides.com. Elapsed time: 5 minutes.

Once we talk about how row types are useful in the abstract, we can move on to the Vinyl library with a few concrete examples. The first slide will have the basic Vinyl type definitions. I'm planning to spend just enough time on this slide to explain the comments above each definition. The second slide will have an example of extracting a subset of fields as a simple way of showing the use case of adding or removing a field. These two slides together should take about 5 minutes. Elapsed time: 10 minutes.

After going over a few examples with Vinyl, I'll talk about how nobody has written a library to perform JSON interchange and API descriptions using row types, and why we might want to be able to use row types throughout our entire program instead of just in the internals. The obvious answer to that is that we want the benefits of row typing at the earliest possible point in our program. The consequences of using row types in this way motivates a key feature: statically defined identifiers for fields within row types. Given this feature, there are even more applications (list the applications: not only JSON field names, but database column names, what else?). This will be one slide with a few bullet points, no code. This portion should take about 5 minutes, but it's okay if it goes a little over, because I'd like some audience participation if possible. Elapsed time: 17 minutes.

Next I'll bring up Composite Records. The first slide will be a newtype definition: newtype s :-> a = Val { getVal :: a }. The second slide will be a definition of Composite Aeson classes. The third slide will be creating a JSON deserialization spec for the User types we defined and then using it to parse JSON. I want to do a specific deserialization spec as opposed to serialization because serialization is not as interesting. Given that there are quite a few confusing types in the first slide I'd like to take a bit of time walking through the actual instance so that those not familiar with recursive typeclasses can grasp the information being presented. The second slide will also take a little bit because the example should get involved. I'm not planning to do any live coding but I'm definitely going to try to find a more detailed example than the one I've laid out and I'll show the basic incantation for deserialization (plus any runtime errors). Elapsed time: 30 minutes

Given that I'm bound to go over time, and will need time to answer questions throughout the talk and after, I'm planning to stop here.

Extending to a Hop workshop

At this time, I'm not planning to extend to a Hop workshop, but in the case that there's interest in doing that, these are my thoughts:

  • There's a lot more there we could talk about on the JSON serialization front. JSON specs are profunctors in nature: assuming you want to define your JSON specs in terms of a single Haskell type, deserialization is the contravariant first argument (how do I create this type from my environment), and serialization is the covariant second argument (how do I inject this type into my environment). Layering a JSON format for a newtype is bimapping it, for example.
  • An alternate direction would be to define a Servant API type and using Servant Swagger to define a Swagger spec for the API. This would assume that the participants are at least familiar with using Servant, but not Swagger. This would include:
    • Defining the API type for a simple service (just one endpoint)
    • Defining the necessary Swagger instances, as that's a common use case for integrating to any external library not supported by Composite

A pre-existing example of Composite, Servant, and Swagger (written mostly by me) is here

Vinyl type definitions

-- construct a value
data Rec :: (u -> *) -> [u] -> * where
  RNil :: Rec f '[]
  (:&) :: !(f r) -> !(Rec f rs) -> Rec f (r ': rs)

-- prove a type is in a Rec
class i ~ RIndex r rs => RElem r rs i where
  rlens :: Lens' (Rec f rs) (f r)
type (∈) r rs = RElem r rs (RIndex r rs)

-- prove multiple types are in a Rec
class is ~ RImage rs ss => RSubset rs ss is where
  rsubset :: Lens' (Rec f ss) (Rec f rs)
type (⊆) rs ss = RSubset rs ss (RImage rs ss)

-- prove two Recs are the same
type REquivalent rs ss is js = (RSubset rs ss is, RSubset ss rs js)
type (≅) rs ss = REquivalent rs ss (RImage rs ss) (RImage ss rs)

User example

newtype Username = Username Text
newtype Password = Password Text
type User = '[Username]
type RegisteredUser = '[Username, Password]

resetPassword :: Record RegisteredUser -> Record User
resetPassword = view rsubset

setPassword :: Password -> Record User -> Record RegisteredUser
setPassword pass user = Identity pass :& user

Composite Aeson classes

newtype FromField e a = FromField { unFromField :: Text -> Parse e a } -- Data.Aeson.BetterErrors.Parse

class RecordFromJson rs where
  recordFromJson :: Rec (FromField e) rs -> Parse e (Rec Identity rs)

instance RecordFromJson '[] where
  recordFromJson _ = pure RNil

instance forall s a rs. (KnownSymbol s, RecordFromJson rs) => RecordFromJson (s :-> a ': rs) where
  recordFromJson (FromField aFromField :& fs) =
    (:&)
      <$> (Identity <$> aFromField (pack . symbolVal $ (Proxy :: Proxy s)))
      <*> recordFromJson fs

User JSON example

newtype Username = Username Text
newtype Password = Password Text
type RegisteredUser = '[Username, Password]

registeredUserRecordFromJson :: Rec (FromField Text) RegisteredUser
registeredUserRecordFromJson = (Username <$> asText) :^: (Password <$> asText) :^: RNil
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment