Skip to content

Latest commit

 

History

History
110 lines (79 loc) · 4.03 KB

README.md

File metadata and controls

110 lines (79 loc) · 4.03 KB

Q : Deeply-nested optional-property record value access combinators for the real world.

(Also some other combinators, because I wanted to put them in here, because they're kind of related.)

In our little PL community, the official statement on lenses is that they are considered-harmful; and even though they have in fact been conjured from the void in Elm, they can be uncomfortable to use - and in many cases, it is often easier and much nicer to flatten data structures before operating on them.

But what if you don't want to operate on them, per se? What if you have some gigantic, deeply-nested record structure with a bunch of optional properties - or worse-yet, what if the code that you are writing exists at the intersection of deeply-nested values?

Consider the following scenario: You have a JSON object, and it looks like this:

{
  "user": {
    "settings": {
      "notifications": {
        "activity": {
          "likes": true,
          "follows": true,
          "retweets": {
            "fromFollowers": true,
            "fromFollowing": true,
            "fromEveryone": {
              "includingMuted": true
            }
          }
        }  
      }
    },
    "profile": {
      "handle": "@lambdapriest",
      "homepage": "x.com/lambdapriest"
    }
  }
}

You decode it into a type that matches its structure exactly, because at some point, you're going to need access to enough of the parts of its full representation that it's just a billion times easier to do so. Or maybe you're using some tooling that does type generation for you, and this is what you get for free. For whatever reason, this is what you have. And for that same reason - maybe it's the codegen, maybe it's the API def, whatever - every field is nullable and/or optional, and so have fun with that.

This is not why I created jmpavlick/q; it's what the cool kids call a "post-hoc rationalization". What you see here, is a problem that is desperately hunting for a solution, that was found by a man that was desperately hunting for a problem to solve.

What can it do?

Check it out:

import A

type alias User =
    { ... pretend that I wrote the type for this, ok? }


notifyOnRetweetsIncludingMuted : User -> Bool
notifyOnRetweetsIncludingMuted =
    Maybe.withDefault False <<
        A.qqqqqqq
            .user
            .settings
            .notifications
            .activity
            .retweets
            .fromEveryone
            .includingMuted

Each q in a function's name represents a call to a function whose signature is a -> Maybe b, for some a and some b.

From the A module, the actual value to be applied to the composition is just a value q; from the Q module, it's a Maybe q.

The letters in the function names give you an indicator of how many "maybe" values are involved in the function's params - for instance:

module Q exposing (..)

qq : (a -> Maybe b) -> (b -> Maybe c) -> Maybe a -> Maybe c

The call site for this looks like

nx = Q.qq ...
  • An uppercase Q to represent that the value being operated on is a Maybe
  • Two lowercase qs to represent two optional maps

Similarly, with A:

module A exposing (..)

qq : (a -> Maybe b) -> (b -> Maybe c) -> a -> Maybe c

The call site looks like:

nx = A.qq
  • An uppercase A to represent that the value being operated on is an "actual value" - i.e, not a Maybe
  • Two lowercase qs to represent the optional maps

It's basically just syntactic sugar around Maybe.andThen; and unlike other cursed things that I hold near and dear to my heart, @jfmengels doesn't have an elm-review rule against it yet.

What's the catch?

  • It only goes up to qqqqqqqqqqq, so if you need to map through more than 11 levels, you're out of luck, sorry
  • It's not a lens; it is, however, a glimpse into the void
  • Nobody actually needs this; at least, I hope they don't