> {-# LANGUAGE AllowAmbiguousTypes #-} > {-# LANGUAGE DataKinds #-} > {-# LANGUAGE DeriveGeneric #-} > {-# LANGUAGE FlexibleContexts #-} > {-# LANGUAGE OverloadedLabels #-} > {-# LANGUAGE PartialTypeSignatures #-} > {-# LANGUAGE ScopedTypeVariables #-} > {-# LANGUAGE TypeOperators #-} > {-# LANGUAGE ViewPatterns #-} > module Examples where > > import Data.Row > import qualified Data.Row.Records as Rec > import qualified Data.Row.Variants as Var In this example file, we will explore how to create and use records and variants. -------------------------------------------------------------------------------- LABELS -------------------------------------------------------------------------------- To begin, we will briefly discuss creating labels -- their use will follow. The most basic way to create a label is through construction with a type signature: x = Label :: Label "x" With the above definition, x is a label for the field x. Using type applications, this can be shortened to: x = Label @"x" And with OverloadedLabels, one can just write: #x We will use the OverloadedLabels notation in these examples. -------------------------------------------------------------------------------- LENS -------------------------------------------------------------------------------- Records and variants play nicely with the lens library if we additionally import Data.Generics.Labels from the generic-lens library. Each overloaded label is also a Lens for a record and a prism for variants. Thus, .! can be replaced with ^. and trial' can be made infix with ^?. Additionally, update can be made infix: update #x v r === r & #x .~ v And because of the power of lens, it's easy to make modifications rather than just update: update #x (f (r .! #x)) r === r & #x %~ f Lens is not included with row-types by default, but using it can make row-types much friendlier. For this example module, we'll include a couple of handy lens operations: > import Data.Generics.Labels () > import Data.Generics.Internal.VL.Lens > > infixl 6 & > (&) :: a -> (a -> b) -> b > (&) = flip ($) > (%~) = over -------------------------------------------------------------------------------- RECORDS -------------------------------------------------------------------------------- With some labels defined, let's begin with records. To start, let's create a record representing the Cartesian coordinates of the origin. To do this, we use the .== operator to initialize values in a record, and we separate each initialized value with the .+ operator.Notice that the value level code uses the same operators as the type level code. > origin :: Rec ("x" .== Double .+ "y" .== Double ) > origin = #x .== 0 .+ #y .== 0 Note that, although we wrote the type explicitly, GHC has no problem inferring it exactly. If we show this at the repl, we see: λ> origin #x .== 0.0 .+ #y .== 0.0 Of course, as an extensible record, the order that we build it shouldn't matter, and indeed, it doesn't. Consider the following variation: > origin' :: Rec ("y" .== Double .+ "x" .== Double) > origin' = #y .== 0 .+ #x .== 0 If we show this at the repl, we see: λ> origin' #x .== 0.0 .+ #y .== 0.0 Indeed, the two values are indistinguishable: λ> origin == origin' True Now, let's expand upon our record. Why stop at two dimensions when we can make a record in three dimensions. > origin3D = #z .== 0.0 .+ origin Once again, the type is inferred for us, and the record is exactly as expected. In fact, we can do this generally. The following function takes a name and a record and adds the "name" field to that record with the given name. > named :: a -> Rec r -> Rec ("name" .== a .+ r) > named s r = #name .== s .+ r Note that we require that the record we are naming must not have a "name" field already. Overlapping labels within a single record/variant is strictly forbidden. Let's say we want to get the values out of the record. Simple selection is achieved with the .! operator, like so: λ> origin .! #x 0.0 and we can use this to write whatever we want. Here is a function for calculating Euclidean distance from the origin to a point: > distance :: (Floating t, r .! "y" ≈ t, r .! "x" ≈ t) => Rec r -> t > distance p = sqrt $ p .! #x * p .! #x + p .! #y * p .! #y Once again, the type of distance is entirely inferrable, but we write it here for convenience. This works exactly as expected: λ> distance origin 0.0 λ> distance origin3D 0.0 λ> distance (named "2D" origin) 0.0 Of course, that wasn't very interesting when our only points are at the origin already. We could make new records representing new points, but instead, let's write a function to move the points we have: > move :: (Num (r .! "x"), Num (r .! "y")) > => Rec r -> r .! "x" -> r .! "y" -> Rec r > move p dx dy = Rec.update #x (p .! #x + dx) $ > Rec.update #y (p .! #y + dy) p Here, we're using the Rec.update operator to update the value at the label x by adding dx to it, and then we do the same for y. We can see it work in practice: λ> move origin 3 4 #x .== 3.0 .+ #y .== 4.0 λ> distance (move origin 3 4) 5.0 λ> distance (move (named "2D" origin3D) 5 12) 13.0 Or, with lenses, we could write move as: > moveLensy p dx dy = p & #x %~ (+ dx) & #y %~ (+ dy) So far, we created an origin point in 2d and then one in 3d, but what if we are adventurous mathematicians who want to have points in a space with some arbitrary number of dimensions. We could write out each of the 0s necessary, but there's an easier way to initialize a record: > origin4 :: Rec ("x" .== Double .+ "y" .== Double .+ "z" .== Double .+ "w" .== Double) > origin4 = Rec.default' @Num 0 Finally, we have come to a case where GHC cannot infer the type signature, and how could it! The type is providing crucial information about the shape of the record. Regardless, with the type provided, it works exactly as expected: λ> origin4 #w .== 0.0 .+ #x .== 0.0 .+ #y .== 0.0 .+ #z .== 0.0 While we have added names or further fields, we can also choose to forget information in a record. To remove a particular label, one can use the .- operator, like so: > unName :: HasType "name" a r => Rec r -> Rec (r .- "name") > unName r = r .- #name For larger changes, it is easier to use the restrict function. The following function will take a record that contains both an x and y coordinate and remove the rest of the fields from it. > get2D :: (r ≈ "x" .== Double .+ "y" .== Double, Disjoint r rest) > => Rec (r .+ rest) > -> Rec r > get2D r = Rec.restrict r GHC is a little finicky about the type operators and constraints -- indeed, some slight modifications to the signature can easily cause type checking to fail. However, a type signature is not necessary when using type applications, and the function can instead be written as: > get2D' r = Rec.restrict @("x" .== Double .+ "y" .== Double) r with no trouble. Yet another altnerative is to match directly on the values desired using the :== and :+ record patterns: > get2D'' :: (r ≈ "x" .== Double .+ "y" .== Double, Disjoint r rest) > => Rec (r .+ rest) > -> Rec r > get2D'' ((Label :: Label "x") :== n1 :+ (Label :: Label "y") :== n2 :+ _) > = #x .== n1 .+ #y .== n2 (Note that overloaded labels cannot be used in the patterns, so the notation is unfortunately bloated by types. Also, the type operators are left associated, so the "_" must go on the right, and the type signature is unforunately necessary.) All three of the get2D functions behave the same. -------------------------------------------------------------------------------- VARIANTS -------------------------------------------------------------------------------- Let's move on from records to variants. In many ways, variants are quite similar, as might be expected given that variants are dual to records. The types look almost the same, and some of the operators are shared as well. However, construction and destruction are obviously different. Creating a variant can be done with IsJust: > v,v' :: Var ("y" .== String .+ "x" .== Integer) > v = IsJust #x 1 > v' = IsJust #y "Foo" Here, the type is necessary to specify what concrete type the variant is (when using AllowAmbiguousTypes, the type is not always needed, but it would be needed to e.g. show the variant). In the simple case of a variant of just one type, the simpler singleton function can be used: > v2 = Var.singleton #x 1 Now, the type can be easily derived by GHC. We can show variants as easily as records: λ> v {x=1} λ> v' {y="Foo"} λ> v2 {x=1} Once created, a variant can be expanded by using type applications and the diversify function. > v3 = diversify @("y" .== String) v2 > v4 = diversify @("y" .== String .+ "z" .== Double) v2 λ> :t v4 v4 :: Var ('R '["x" ':-> Integer, "y" ':-> String, "z" ':-> Double]) λ> v == v3 True The diversify function makes use of the .\/ type class, pronounced min-join. The min-join of two row-types is the minimum row-type that contains all the bindings of the two constituent ones. This allows use to write a function to join two lists of variants: > joinVarLists :: forall x y. (WellBehaved (x .\/ y), x .\/ y ≈ y .\/ x) > => [Var x] -> [Var y] -> [Var (x .\/ y)] > joinVarLists xs ys = map (diversify @y) xs ++ map (diversify @x) ys Unfortunately, GHC cannot deduce that the min-join of x and y is the same as the min-join of y and x, so we must add that to the constraints. However, any concrete types x and y that we construct will have this property, so it is easy to dispatch when we go to use this function. Taking a step back, it's worth looking closer at the equality tests we did earlier on variants. Indeed, one may ask how equality works on variants at all. For instance, v2 and v3 both look the same when you show them, and they both have the same value inside, but can we test them for equality? Indeed, we can't, precisely because their types are different: it is a type error to even try to check whether they're equal: λ> v2 == v3 error: • Couldn't match type ‘'["y" ':-> [Char]]’ with ‘'[]’ Expected type: Var ('R '["x" ':-> Integer]) Actual type: Var ('R '["x" ':-> Integer] .+ ("y" .== String)) • In the second argument of ‘(==)’, namely ‘v3’ In the expression: v2 == v3 In an equation for ‘it’: it = v2 == v3 This may look a little scary, but it's actually a pretty useful message. Essentially, it's expecting a variant that can only be an Integer at label "x", but it found one that could also be a String at label "y". So, comparing v2 and v3 is not allowed, but since v3 now has the same labels as v1, that comparison is fine: λ> v == v3 True λ> v == IsJust #x 3 False λ> v == v' False λ> v == IsJust #y "fail" False (Also note here that using IsJust without a type signature is fine because the correct type can be easily inferred due to v's type.) What can you do with a variant? The only way to really use one is to get the value out, and to do that, you must trial it: λ> trial v #x Right 1 λ> trial v #y Left {x=1} λ> trial v' #x Left {y="Foo"} λ> trial v' #y Right "Foo" If trialing at a label l succeeds, then it provides a Right value of the value at l. If not, it provides a Left value of the variant with this label removed---since the trial failed, we now can be sure that the value is not from l. -------------------------------------------------------------------------------- Note on lenses: The generic-lens library distinguishes labels that are meant to be lens from labels meant to be prisms by whether the front of the label is an underscore followed by an uppercase letter. This makes a lot of sense for data constructors, which is what generic-lens's prisms were designed for, but it's a little restrictive for variants. The result is that we can only use the lensy notation if the labels in our variants are uppercase. Consider the following: > vUpper :: Var ("Y" .== String .+ "X" .== Integer) > vUpper = IsJust (Label @"X") 1 λ> v ^? #_X Left 1 The row-types library does not generally assert that variants need labels that start with uppercase letters while records need labels that start with lowercase letters---in fact, the `switch` function described below will only work if the labels in a record and variant are exactly the same---but GHC is limited in that the # syntax only works for lowercase labels. Therefore, to make uppercase labels like in the `vUpper` example above, one must use the syntax `Label @"X"` instead of simply `#X`. See the proposal in https://github.com/ghc-proposals/ghc-proposals/pull/170 for more information. -------------------------------------------------------------------------------- For ease of use in view patterns, Variants also exposes the view function. (If using lens, this can be replaced with preview.) With it, we can write a function like this: > myShow :: (r .! "y" ≈ String, Show (r .! "x")) => Var r -> String > myShow (Var.view #x -> Just n) = "Showable of "++show n > myShow (Var.view #y -> Just s) = "String of "++s > myShow _ = "Unknown" λ> myShow v "Showable of 1" λ> myShow v' "String of Foo" λ> myShow (just #z 3 :: Var ("y" .== String .+ "x" .== Integer .+ "z" .== Double)) "Unknown" This can also be achieved with the IsJust pattern synonym in much the same way: > myShow' :: (WellBehaved r, r .! "y" ≈ String, Show (r .! "x")) => Var r -> String > myShow' (IsJust (Label :: Label "x") n) = "Showable of "++show n > myShow' (IsJust (Label :: Label "y") s) = "String of "++s > myShow' _ = "Unknown" In either case, the type signature is once again totally derivable. There are three minor annoyances with this. First, it's annoying to have to write out the Label types in the pattern. This is actually a requested issue on GHC (see https://gitlab.haskell.org/ghc/ghc/issues/13116 and https://github.com/ghc-proposals/ghc-proposals/pull/80 for more information). Second, it's fairly common to want to define a function like myShow to be exhaustive in the variant's cases, but to do this, you must manually provide a type signature: > myShowRestricted :: Var ("y" .== String .+ "x" .== Integer) -> String > myShowRestricted (Var.view #x -> Just n) = "Integer of "++show n > myShowRestricted (Var.view #y -> Just s) = "String of "++s > myShowRestricted _ = error "Unreachable" The final blemish can be seen in this restricted version of myShow. Even though we know from the type that we've covered all the posibilities of the variant, GHC will generate a "non-exhaustive pattern match" warning without the final line. (This is true for the pattern synonym version too.) One way to avoid this problem is to use switch. The switch operator takes a variant and a record such that for each label that the variant has, the record has a function at that label that consumes the value the variant has and produces a value in a common type. Essentially, switch "applies" the variant to the record to produce an output value. > --myShowRestricted' :: Var ("y" .== String .+ "x" .== Integer) -> String > myShowRestricted' v = switch v $ > #x .== (\n -> "Integer of "++show n) > .+ #y .== (\s -> "String of "++s) This version of myShow needs neither a type signature (it is inferred exactly) nor a default "unreachable" case. However, we no longer have the benefit of Haskell's standard pattern matching. A more powerful version of trial is multiTrial, which tests for multiple labels at once. With this, you can wholesale change the type of the variant to any (valid) variant type you would like. Of course, there needs to be a recourse if the variant you provide is not expressible in the type you want, so multiTrial returns an Either of the type you want or a Variant of the leftovers. Consider the examples: λ> :t multiTrial @("x" .== Double .+ "y" .== String) v multiTrial @("x" .== Double .+ "y" .== String) v :: Either (Var ('R '["x" ':-> Integer])) (Var ('R '["x" ':-> Double, "y" ':-> String])) λ> multiTrial @("x" .== Double .+ "y" .== String) v Left {x=1} λ> :t multiTrial @("x" .== Double .+ "y" .== String) v' multiTrial @("x" .== Double .+ "y" .== String) v' :: Either (Var ('R '["x" ':-> Integer])) (Var ('R '["x" ':-> Double, "y" ':-> String])) λ> multiTrial @("x" .== Double .+ "y" .== String) v' Right {y="Foo"} Thus, multiTrial can be used not only to arbitrarily split apart a variant, but also to change unused label associations (in this case, we changed the variant from one where "x" is an Integer to one where it's a Double). We can even use it to combine dispatching of two different variants at once: > also :: Disjoint xs ys > => (Var xs -> a) > -> (Var ys -> a) > -> Var (xs .+ ys) -> a > also f1 f2 e = case multiTrial e of > Left e' -> f1 e' > Right e' -> f2 e' The above also function takes two functions f1 and f2 that can each independently be used on variants with rows xs and ys respectively. Using multiTrial, we can split the input variant (which is the join of xs and ys) and easily apply f1 or f2 as appropriate.