Kinda ironic that given the problem definition no solution has been provided on XQuery or XSLT yet, the widely used domain specific languages that better suit the problem :-).
I believe there's a lot of inspiration to be had from the X-family of languages. The "secret sauce" is XPath. We need a new language drawing inspiration from the tree processing of X* languages but with more modern syntax and less with a less byzantine type system (read: anything but XML Schema :-). Less angle brackets would help too!
---
Bonus points: add tuples and prolog like queries to such language (thinking of SPARQL) ... now that would be something really powerful.
This exists (optics/lenses), but most programmers don't know about it. I expect this to hit mainstream programming languages a few years later.
I don't know of a simple, jargon-free explanation of the concepts. WP is useless, sadly. http://enwp.org/Bidirectional_transformation
Let me have a go! I’d explain optics/lenses as giving field access as a data structure. That is, a lens is an object which describes how to access an element from a ‘larger’ value. For example, you can define _1 and _2 as lenses for accessing the first and second element respectively of a tuple:
_1 :: Lens' (a,b) a
_2 :: Lens' (a,b) b
(Using Haskell syntax here as that’s what I know best, though I’ve simplified the types slightly.)The great advantage of having these as first-class values is that you can now manipulate them. Of course, the main things you’ll want to do with these are get and set things:
> (1,2) ^. _1
1
> (1,2) & _1 .~ 3
(3,2)
More interestingly, you can also use a function to modify the value to which a lens is pointing: > (1,2) & _1 %~ negate
(-1,2)
Most usefully, you can also compose lenses. For instance, to get the second element of the first element of a nested tuple, you can compose _2 and _1 using dot notation: _1._2 :: Lens' ((a,b),c) b
Or, for a more realistic example, here’s a lens which accesses the ‘numSeen’ field of the value corresponding to key "keyToIncrement": (ix "keyToIncrement")._numSeen :: Lens' (Map String MyStructure) Int
(Well, strictly speaking, this is actually a traversal or a prism or something, because "keyToIncrement" might not exist, but let’s ignore that for this really simple example…)And now that you have it, you can use this new lens to do anything you want with this value: you can get it, set it, increment it, decrement it, print it, etc.
Additionally, you don’t have to restrict yourself to just lenses. For example, ‘traversals’ are a generalisation of lenses which allow you to access 0, 1 or more elements, rather than restricting access to just one element at a time. For instance, you can use ‘both’ to transform, well, both elements of a tuple:
> (1,2) & both 5~ negate
(-1,-2)
Or you can use ‘traversed’ to access each element of, say, a list: > [1,2,3,4,5] & traversed .~ 0
[0,0,0,0,0]
Of course, there are other ways of doing each of these. If you happen to be working with mutable values, you can sometimes use dot notation to access deeply nested fields. Some usecases of ‘traversed’ can be replaced by a map. However, there really is no comparable substitute for lenses when working with deeply nested immutable structures, especially ones of unknown shape. _1 :: Lens' (a,b) a
_2 :: Lens' (a,b) b
From the looks of it, this is to access the first and second element specifically of a 2-tuple. I suppose if you had to generalize it, you would actually get a traversal or a prism or something, because those elements might not exist.Yes, this is correct. (I don’t know about other languages, but it’s worth noting that in Haskell, tuples are of fixed size — 2-tuples, 3-tuples etc. are considered completely different types. A result of this is that ‘tuple of any length’ is not a valid concept in Haskell; you have to write a separate definition for each tuple size.)
> I suppose if you had to generalize it, you would actually get a traversal or a prism or something, because those elements might not exist.
Well, it depends how you generalise it. Haskell lens libraries, for instance, use a more general type which allows _1 and _2 to work with 2-tuples and larger, _3 to work with 3-tuples and larger, _4 to work with 4-tuples and larger, and so on. Thus these remain lenses, as they always access exactly one value.
Another generalisation is to define a lens allowing access of an arbitrary element of a list. This may not return a value, and thus is indeed a traversal.
A lens is a (1) functional (2) bi-directional (3) first-class and (4) composable generalization of (5) struct field names in most traditional languages.
1) "Functional" meaning we eschew mutation and implicit context. Mutation is a very powerful feature of a language, and much can be encoded using it. When we remove mutation as an option, many of the things we would have just used mutation for separate into distinct and rich solution spaces.
2) "Bi-directional" meaning we can use the same widget for both accessing a sub-part of a whole, and for replacing that sub-part within a whole. I personally call these operations "extract" and "replace", since "get" and "set" feel like they raise the specter of mutability.
3) "First-class" means that the lenses themselves are manipulable constructs within the language. In a language like C or Java, I can have "obj.a.b.c", but I cannot have ".a.b.c" itself on its own. Even if you consider method references (like "Obj::a" in Java), you've only obtained the ability to extract the "a" field from an Obj -- any particular method or function is only going to go in one direction.
4) "Composable" means that you can take two lenses and fit them together to get a new lens. Conceptually, I think this is understood -- we intuitively understand how to navigate an object graph when writing down an expression like "obj.a.b.c" -- but having a composable first-class abstraction lets us do the same thing programmatically. (It need not even be at "runtime" -- imagine a compile-time macro system that programmatically generates nested accesses from some base rules you define. I think you've got the rudiments of a dependency injection system here.)
5) "Struct field" means that for the given type, the field is always accessible. "List element" (or "tree leaf", ...) would give you traversals instead of lenses, and "tagged union variant" / "subclass" would give you prisms.
As for potential applications of the approach, there's the approach dependency injection graphs outlined above. I've also used ideas from lenses/prisms in serialization/deserialization and web service middleware. (Middleware compose really well as functional optics!)
With profunctor optics, you can even construct self-describing operations. For instance, imagine a JSON parser that can be directly interrogated to obtain the schema it accepts, or a middleware stack that can be interrogated to determine which headers it looks at, and which routes it accepts. Personally, this is the area I'm most excited about with functional optics.
> With profunctor optics, you can even construct self-describing operations. For instance, imagine a JSON parser that can be directly interrogated to obtain the schema it accepts, or a middleware stack that can be interrogated to determine which headers it looks at, and which routes it accepts. Personally, this is the area I'm most excited about with functional optics.
I’m not sure this has anything to do with profunctor optics per se; you can do it with any structure which is polymorphic enough. (A neat example I saw recently: Build Systems à la Carte [0] allow you to get a list of dependencies from a build task, as long as that task is Applicative i.e. does not have dynamic dependencies.)
[0] https://www.microsoft.com/en-us/research/uploads/prod/2018/0...
Thank you!
> I'm not sure this has anything to do with profunctor optics per se
Yes, fair! I didn't mean to imply that only profunctor constructions can be self-descriptive. Rather, I meant that optics based solely on the function arrow don't really give you anywhere to annotate with description in the first place. By assuming less about the operations they're lifting, profunctor optics give you more freedom to model a self-descriptive profunctorial operation.
In the `(a -> f b) -> (s -> f t)` formulation, the lens takes a function and returns an adapted function. There's nowhere to put additional information, and even if you could stash it on the `f`, you'd have to feed it an `s` just to get at it.
In the `p a b -> p s t` formulation, the function arrow (if it's used in your chosen `p` at all!) doesn't have to be the top-level type you return; you could have something like `(a -> f b, Metadata)` instead. You can add combinators to your concrete `p` that manage the metadata and let you combine multiple such operations, and intermix them freely with profunctor-generic lenses that don't care what concrete profunctor you're using. In the end, you get a `p a b` with the metadata immediately available and not necessarily hidden under a function application.
This distinction only comes up because lenses are polymorphic in both the input and output types. The example of a JSON parser is only polymorphic in its output type (we can assume the input is always some String), so you can easily get a `Parser b` with all the metadata at the surface. Since Profunctors have two type parameters, one covariant and one contravariant, we can build models that vary over the input type while retaining the ability to be self-descriptive.
I read a blog post recently [0] that showed some examples of using Profunctors in this way. It also shows the relationship between Profunctor and Arrow; the latter is basically a Strong Profunctor (you can turn a `p a b` into a `p (c, a) (c, b)` that threads a `c` through unaltered) where you can also compose Profunctor elements directly (`p a b -> p b c -> p a c`). In the end, you get an entire operation `p a z` in your Profunctor, with any statically observable metadata ready to be extracted from its structure.
[0] https://elvishjerricco.github.io/2017/03/10/profunctors-arro...
The core problem would be you can't write something like this
fn return_part() -> &Part {
let whole = get_whole();
&whole.part
}
because whole is "dropped" at the end of the function call.If you want to hand out parts you have to put the whole somewhere where it will stick around until you're done with the parts, and doing that nicely pretty much requires GC (you could also use lenses only in very restricted scopes or just leak memory).
The XSLT+XPath approach is more dynamic, easier to grasp (imho) and can probably be learned in an afternoon.
Tradeoffs, tradeoffs... I wouldn't want to write most programs in Haskell but you could write pretty much any program in, say, JavaScript ... :-)