3 ms·
Lenses begin with getting and setting. Since Haskell is immutable, setting is a little different—it's a method of quickly generating a mutation function for a t
by tel 13y ago
Lenses begin with getting and setting. Since Haskell is immutable, setting is a little different—it's a method of quickly generating a mutation function for a type. Lenses keep getting and setting bound together as a first-class value which creates the idea of it being a value representing a "focus" on a particular field in a complex, nested value.
Then you can compose these lenses together and retain these properties. You can also pick lenses which operate with different multiplicities from 0 to infinity. Finally, you can use their seemless connection to object isomorphisms to create a very general interface for working with various kinds of "similar" objects in Haskell.
At the end of the day you can write a first-class lens which represents getting and setting over a set of parameterized Map keys deeply inside of a stateful computation, mapping over anything that looks like a string, and viewing/modifying it as decoded JSON.
obj . ix "aKey" . each . _Object . ix "3" . _Number .~ 4
might modify a record in a type like this
IsString s => SomeState { obj :: Map s s }
where the string values of the Map have a JSON schema like
{"1" ..., "2" ..., "3": <aNumber> }
- bjourne 13y agoBut you haven't presented anything that is special about lenses. For example, your lens expression would be trivially translated to this Python: for o in obj["aKey"]: o["3"] = 4 The OP:s point was that lenses seem like a workaround for Haskell's lack of mutable state. EDIT: What I'm meaning is that to show lenses unique benefits, you would have to come up with an example in which the code is more succinct than the equivalent code implemented without lenses in another language.
- tel 13y agoBut, that isn't anything resembling a faithful translation. It's non-first class, non compositional, only a "setter", lacks a decent error handling code (I didn't mention that, but it's built into "mistargeted" lenses) and depends upon parsing the JSON in some other step. Lenses let you think of a JSON-encoded string as an actual JSON string without ever explicitly doing the decoding due to their close connection to Isomorphisms and "partial Isomorphisms" (called, non-standardly, Prisms here). Furthermore, lenses don't really have anything to do with mutable state—they just happened to form a convenient wrapper for using the State monad, but that's really a coincidence. Succinctness is difficult to grasp. It'd be a good exercise that I'm not going to try in an HN comment to even translate the entirety of the concept embedded in that one line into, say, Python. It would start to feel like an XPath implementation. (Edit: Also, as usual, the whole typesafe thing. That Python fragment can lead to runtime errors. The Haskell one never does—using it inappropriately is simply impossible.)
- bjourne 13y agoStill, you haven't shown why anyone should be impressed by Haskell's lenses. It's like you do not understand that isomorphisms and first-classness is totally irrelevant to me and most programmers unless it leads to better code. Here is a challenge for you or anyone else who loves lenses: Take a small snippet of real source code you or anyone else has written and uses lenses and post it here. I'll then translate it into Python that has the equivalent effect. If the translation is impossible or is less pretty than the Haskell original, I'll award you $1000 internet points.
- edwardkmett 13y agoimport Control.Lens and let us use two components from lens: rewriteOf :: Setter' a a -> (a -> Maybe a) -> a -> a which can take a rewrite rule and apply it to any 'self-similar' setter recursively in a bottom up fashion until it ceases to apply and uniplate :: Data s => Traversal' a a that says that if we have an instance for Haskell's built in generic programming framework 'Data', we can get a traversal of the immediate descendants. Now rewriteOf uniplate $ \case Neg (Lit a) -> Just $ Lit (-1) _ -> Nothing will walk a syntax tree looking for negated literals starting recursively from the bottom of the tree, applying that rewrite rule on the right hand side until it no longer applies and fold it back up the tree. This works in a lazy setting where you can have potentially infinitely many targets and you didn't have to write any code to define the traversal. The data type itself was just something like: data Term = Var String | Neg Term | Lit Int | App Term Term | Abs String Term deriving Data
- tel 13y agoLet's say you've serialized a tree structure of versioned data as JSON. Branches are arrays and leaves are objects. data OTree = Obj Object | Node (Data.Vector.Vector OTree) instance FromJSON OTree where parseJSON (Array as) = Node <$> traverse parseJSON as parseJSON (Object o) = return (Obj o) parseJSON _ = fail "OTrees are objects and arrays" instance ToJSON OTree where toJSON (Node as) = Array (fmap toJSON as) toJSON (Obj o) = Object o Now some of these objects have keyed "version"s which are arrays of semantic versioning numbers. Write a function which decodes and re-encodes a new tree with each of these semantic versioning numbers incremented at the patch level. If the version isn't in that format, just ignore it. In Haskell you'd want to write a generic traversal over the objects of the tree useful whenever you want even access to the contained elements. eachObj :: Traversal' OTree Object eachObj inj (Obj o ) = Obj <$> inj o eachObj inj (Node as) = Node <$> traverse (eachObj inj) as And then here's the finale, the actual lens code specific to the task. upgrade = _JSON . eachObj . ix "version" . _Array . ix 2 . _Number +~ 1