Highly, terribly dangerous posts. Misuse of this profile can invite eggbug to trounce upon your data and then laugh in your face. You don't want this profile. Really.



Profpatsch
@Profpatsch

When you write business-logic in Haskell, you oftentimes have some intermediate results in your long-ish functions that are n-tuples of mostly the same type, or maybe a Map Text Text or a Either (Either Foo Foo) Foo.

Of course you can always create newtype wrappers for everything, but what if there was a generalized way to do that for simple cases. Well there is!

When we just need a simple “label” for the types we are dealing with, we can use the code above. So the Map Text Text above would become something like Map (Label "objectId" Text) (Label "description Text). And all related values will show these annotated types as well, making it harder to accidentally confuse values.

As an added benefit, via the HasField instance we can just use dot syntax to unwrap the values again! And it uses the label name!! This is pretty much the best possible interface imho.

The only missing piece would be a pattern synonym for unwrapping the label, but I haven’t figured out how to do that yet while also requiring the label name to be written in the pattern match. Something like

case obj of
  Left (Label @"objectId" objId) -> …
  …

would be ideal, but I don’t think it’s possible to require the type argument to be there.

Other than that, pretty neat, I think we are gonna use that a lot.

Oh, and since it’s a newtype it’s also a so-called “zero cost abstraction”. Fancy!

(Background: we originally used superrecord, which is a full-blown anonymous record library, but noticed it had a few bugs & triggered GHC bugs as well, so this gives us 80% of the benefits without any of the complicated stuff)


You must log in to comment.

in reply to @Profpatsch's post:

This is really cool!

Your pattern synonym is possible with a little type class magic

newtype Label (label :: Symbol) value = UnsafeLabel value

class SameLabel a b
-- GHC is not smart enough to deduce that SameLabel is 
-- really just an equality constraint
instance SameLabel a a

pattern Label :: forall (proxyLabel :: Symbol) label value
               . SameLabel proxyLabel label 
               => value 
               -> Label label value
pattern Label value <- UnsafeLabel value

someLabel = label @"test" (5 :: Int)

-- This compiles
case someLabel of
    Label @"test" x -> x
-- This doesn't
case someLabel of
    Lable x -> x