Inessential guide to fclabels

Last time I did an Inessential guide to data-accessor and everyone told me, "You should use fclabels instead!" So here's the partner guide, the inessential guide to fclabels. Like data-accessor the goal is to make record access and editing not suck. However, it gives you some more useful abstractions. It uses Template Haskell on top of your records, so it is not compatible with data-accessor.

Identification. There are three tell-tale signs:

Type signatures that contain :-> in them ("Oh, that kind of looks like a function arrow... but it's not? Curious!"),

Records that contain fields with a leading underscore (as opposed to data-accessor's convention of an trailing underscore), and

An import Prelude hiding (id, (.), mod), with an import from Control.Category to replace them.

Interpreting types. A label is signified by r :-> a which contains a getter r -> a and a setter a -> r -> r. Internally, a wrapped label is simply a point, a structure consisting of r -> a and b -> r -> r, with a required to be equal to b. (As we will see later, a point is useful in its own right, but not for basic functionality.)

Accessing record fields.

get fieldname record

Setting record fields.

set fieldname newval record

Modifying record fields. For fieldname :: f a :-> a, modifier should have type a -> a.

mod fieldname modifier record

Accessing, setting and modifying sub-record fields. Composition is done with the period operator (.), but you can't use the one from the Prelude since that only works with functions. The composition is treated as if you were you composing the getter.

Accessor over applicative. You can use fmapL to lift an accessor into an applicative context. This is useful if your record is actually Maybe r (You can turn r :-> a into Maybe r :-> Maybe a).

But wait, there's more!

More fun with views. Remember that a point is a getter and a setter, but they don't have to be for the same types. Combined with a clever applicative instance, we can use this to incrementally build up a label composed of multiple labels. The result looks a lot like a view that you'd be able to create on a relational database. The recipe is:

Have the constructor for the resulting type (e.g. (,), the tuple constructor),

Have all of the accessors for the resulting type (e.g. fst and snd), and

Have the labels you would like to compose together (say, label1 and label2).

Combine, with for, each accessor for the resulting type (2) with the label to be accessed with that accessor (3), combine all of these resulting points with the constructor for the resulting type with the applicative instance, i.e. <$> and <*>, and then stick it in a label with Label:

(,) <$> fst `for` label1 <*> snd `for` label2

Amazingly, you won't be able to mix up which argument an accessor (2) should be placed in; the result won't typecheck! (See the Postscript for a more detailed argument.)

More fun with lenses. A function implies directionality: a to b. But light can filter through a lense either way, and thus a lense represents a bidirectional function. We can apply filter a label f :-> a through a lense a :<->: b to get a new label f :-> b (remember that composition with a regular function is insufficient since we need to put values in as well as take values out). One has to be careful about what direction your lense is pointed. If label :: r :-> a, in :: b -> a and out :: a -> b, then:

which is equivalent to Point Person (a, b) (a, b), which is a valid Label.

But what is for doing? The source code documentation says:

Combine a partial destructor with a label into something easily used in the applicative instance for the hidden Point datatype. Internally uses the covariant in getter, contravariant in setter bi-functioral-map function. (Please refer to the example because this function is just not explainable on its own.)

Well, I'm going to ignore this advice, since you've seen the example already. Let's parse this. for is covariant in getter r -> a and contravariant in setter a -> f -> f. These terms are from category theory describing functors. A covariant functor is a "normal" functor, whereas a contravariant functor is one with composition flipped around. So while normally fmap f g == f . g, in the contravariant world fmap f g == g . f:

@Edward Z. Yang: whilst it’s true fclabels is built on top of records to get the TH/deriveAccessors part working, there’s no reason one can’t write the necessary instances manually. As such there’s no need for record syntax to get fclabels to work. Or am I missing something?

Sure. I guess the real point here is, you want someone to do the deriving for you, and the deriving has to be based off of some data, and that data will essentially be some sort of record system. I don’t think I was claiming you need record syntax (except that you need a way of expressing primitive setters.)