Last active
January 25, 2019 23:06
-
-
Save glebec/ec79b56a9bd4fe75d35fb057953dbd82 to your computer and use it in GitHub Desktop.
Lenses in JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Van Laarhoven Lenses in JavaScript | |
* by Gabriel Lebec (https://github.com/glebec) | |
* | |
* Based on https://www.codewars.com/kata/lensmaker | |
* See also https://github.com/ekmett/lens/wiki/History-of-Lenses | |
*/ | |
/** | |
* Composition and Combinators | |
*/ | |
// the K combinator, creates a function "fixated" on a value | |
const konst = a => _ => a | |
// the C combinator, flips a binary function's inputs | |
const flip = f => a => b => f(b)(a) | |
// the B combinator, composes two functions (right-to-left) | |
const compose = f => g => a => f(g(a)) | |
// left-to-right function composition | |
const pipe = flip(compose) | |
// abusing function prototype to have infix function composition | |
Function.prototype.c = function(g) { | |
return compose(this)(g) | |
} | |
Function.prototype.p = function(g) { | |
return pipe(this)(g) | |
} | |
/** | |
* Two Key Functors | |
*/ | |
// The Identity functor just maps its internal value. It's fairly trivial. | |
class Identity { | |
constructor(a) { | |
this.val = a | |
} | |
static of(...args) { | |
return new Identity(...args) | |
} | |
map(f) { | |
return Identity.of(f(this.val)) | |
} | |
} | |
// The Const functor ignores mapping. It's useful for hanging onto a result. | |
class Const { | |
constructor(a) { | |
this.val = a | |
} | |
static of(...args) { | |
return new Const(...args) | |
} | |
map(_) { | |
return this | |
} | |
} | |
/** | |
* Lens Functions | |
* | |
* Lens s a :: Functor f => (a -> f a) -> (s -> f s) | |
* | |
* A lens is a single function which acts as both a getter and a setter | |
* (depending on how it is used). The lens itself is not used directly on | |
* the datatype of interest, but rather specialized with the use of helper | |
* functions like `view`, `over`, and `set` which act on lenses to produce | |
* getters & setters. | |
* | |
* `Lens s a` can be read as "a lens between container `s` and focus `a`". | |
*/ | |
// `view` specializes the lens to be a getter. | |
// view :: Lens s a -> s -> a | |
// view :: Functor f => ((a -> f a) -> (s -> f s)) -> s -> a | |
const view = lens => s => lens(Const.of)(s).val | |
// `over` specializes the lens to be an immutable transformer. | |
// over :: Lens s a -> (a -> a) -> s -> s | |
// over :: Functor f => ((a -> f a) -> (s -> f s)) -> (a -> a) -> s -> s | |
const over = lens => a2a => s => lens(a2a.p(Identity.of))(s).val | |
// `set` specializes the lens to be an immutable setter. | |
// set :: Lens s a -> a -> s -> s | |
// set :: Functor f => ((a -> f a) -> (s -> f s)) -> a -> s -> s | |
const set = lens => a => over(lens)(konst(a)) | |
/** | |
* Example Lenses and Demonstrations | |
*/ | |
// pair :: [a, b] | |
// _1 :: Lens [a, b] a // a lens from pairs to the first element | |
// _1 :: Functor f => (a -> f a) -> [a, b] -> f [a, b] | |
const _1 = a2fa => ([a, b]) => a2fa(a).map(a2 => [a2, b]) | |
// _2 :: Lens [a, b] b // a lens from pairs to the second element | |
// _2 :: Functor f => (b -> f b) -> [a, b] -> f [a, b] | |
const _2 = b2fb => ([a, b]) => b2fb(b).map(b2 => [a, b2]) | |
// Notice that we are taking a pair, [a, b], placing one element into some | |
// functor `f`, and calling `map` to build a new pair with one element changed. | |
// Note, if the functor used is `Const`, the call to `map` is effectively | |
// ignored and instead we keep the extracted value as-is (wrapped inside the | |
// `Const` functor). The function `view` uses `Const` and returns the value | |
// wrapped inside that functor. | |
console.log(view(_1)([true, false])) // true | |
console.log(view(_2)([true, false])) // false | |
// However, if the functor used is `Identity`, this map succeeds and we have | |
// built an immutably-updated new pair. The function `set` uses `Identity` and | |
// returns the (new) value from inside that functor. | |
console.log(set(_1)('hi')([true, false])) // ['hi', false] | |
console.log(set(_2)('yo')([true, false])) // [true, 'yo'] | |
/** | |
* Composing Lenses | |
* | |
* Because lenses are just functions, they compose directly. | |
* | |
* Lens s a :: Functor f => (a -> f a) -> (s -> f s) | |
* Lens a x :: Functor f => (x -> f x) -> (a -> f a) | |
* Lens s a . Lens a x = Lens s x | |
* | |
* This creates a composite lens from `s` focused on a deeply-nested `x`. | |
*/ | |
const _1_1_1 = _1.c(_1).c(_1) | |
const _2_2_2 = _2.c(_2).c(_2) | |
console.log(view(_1_1_1)([[[true]]])) | |
console.log(view(_2_2_2)([false, [false, [false, true]]])) | |
console.log(view(_1.c(_2).c(_1))([[false, [true]]])) | |
// Of course, merely accessing nested data in JS is easy. This is comically | |
// overwrought for that use case. But what about immutable update? Still works! | |
console.log(set(_1_1_1)(5)([[[true, false], false], false])) // [[[5, false], false], false] | |
/** | |
* Idiomatic JS Use Case: Objects | |
*/ | |
// `prop` is a lens factory – a function for creating lenses. | |
// prop :: String -> Lens Object a | |
// prop :: Functor f => String -> (a -> f a) -> (Object -> f Object) | |
const prop = key => a2fa => obj => | |
a2fa(obj[key]).map(a => ({ ...obj, [key]: a })) | |
// Some example lenses | |
const friend = prop('friend') | |
const name = prop('name') | |
const pet = prop('pet') | |
// Sample data | |
const person = { | |
name: 'Old MacGregor', | |
friend: { | |
name: 'Old MacDonald', | |
pet: { | |
name: 'Miss Piggy', | |
age: 'nunya business', | |
}, | |
}, | |
} | |
const friendPetName = friend.c(pet).c(name) | |
// viewing deeply nested data works fine… | |
console.log(view(friendPetName)(person)) | |
// but so does updating! | |
console.log(set(friendPetName)('Kermit')(person)) | |
// and mapping. | |
console.log(over(friendPetName)(s => s + '!!!')(person)) |
@rockymadden glad to be of help. I should add that in a typed functional setting, lens signatures are usually formulated as Lens s t a b
(two extra type parameters), which can be read as "a lens from container s
to focus a
, which can be transformed into a container t
with focus b
". This allows lenses to be used in an even more general way. In JS we can change the types of things with zero ceremony, so I didn't use the full type signature; an Object whose .age
is a String is considered the "same type" as an Object whose .age
is a Number.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Fantastic, thank you! Have some complementary lens operations in a local lib leveraging Ramda and Sanctuary-Def. I was attempting to break down and enforce the signature of the lens itself and just so happened to come across your profile and this gist via https://gist.github.com/Avaq/1f0636ec5c8d6aed2e45#gistcomment-2776771.