Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Type-checked keypaths in Rust (cmyr.net)
81 points by goranmoomin on June 27, 2022 | hide | past | favorite | 20 comments


I created something similar for our project “below” (https://github.com/facebookincubator/below/blob/main/below/b...).

The program collects system resource metrics into a data structure and we need to display the fields with different styles and formats. In order to decouple the data structure from rendering, Queriable (Keyable) and FieldId (combine KeyPath + mirror struct into enum) are used. I will definitely like to checkout the KeyPath implementation as it seems more general.


Typed Keypaths are one of the Swift language features which I really miss in any other language. They're incredibly powerful because it is possible to compose them. Imagine a struct or class `Item` with a `user` field and the `User` has an `age` of `Int`:

    // Keypath<Item, User> (from Item to User)
    let item_user = \Item.user
    // Keypath<User, Int> (from User to Int)
    let user_age = \User.age

    // Keypath<Item, Int> (from Item to Int)
    let item_age = item_user.appending(user_age)


Most languages with good support for functional programming have lenses, which generalize keypaths. Off the top of my head: Haskell, Purescript, TypeScript, Scala, OCaml, Racket, and Common Lisp all have lens libraries.


For (typed) optics to work well you need HKTs and universal quantification over those types, which rules out many of the languages you listed. Haskell and PureScript should be fine. I don't see how it would work in TS, scala, ocaml. Lisp should work, but no types right? No idea about racket.


For general optics, yes. But lenses and to a lesser extent prisms still work ok in their naive getter/setter and constructor/pattern encodings.


scala has HKTs and should be able to implement lenses just fine, I think. TS and OCaml can emulate HKTs in a really awkward way, but they can also just implement a less general more restricted subset.


IMHO the issue is that lenses are too powerful. For this reason, they are liable to be used in overly clever ways that reduce readability and increase complexity. Things like keypaths are useful because they are (intentionally) more restricted than lenses, and are thus less likely to be misused.


In c++:

    auto item_user = [](auto&& item) { return item.user; };
    auto user_age = [](auto&& user) { return user.age;  };
    auto item_age = bind(user_age, bind(item_user, _1));
But surely there must be more to keypaths than lambdas and function composition? (You do not have to use bind of course).


I think keypaths also permit writing (but maybe the rvalue-reference does that?). And probably they allow using properties and not just fields, but I don't know Swift tbh.

But this touches on one thing I think would be interesting in a language. On the one hand you can define a function like you did, which is a black box that does something. On the other hand, you can talk about "the action of doing something", and assign that description to a variable, introspect it, change the action. Maybe convert from imperative to command-pattern, so you can undo, and so on. I think one of the few languages that allows something like this is Lisp. Keypaths are somewhat declarative, but don't allow introspection and are limited to getting/setting it seems.


During return type inference, references decay to value types by default, so you have to use some awkward syntax if you want to use them to set values:

   auto item_user = [](auto&& item) -> decltype(auto) { return (item.user); };

   item_user(item) = my_user;
Even more awkward if you want some slightly better error messages with concepts:

   auto item_user = [](auto&& item) -> decltype(auto) requires requires { item.user;}  { return (item.user); };
(yes, the double requires is not a mistake).

At least is very macroable, which would also allow to wrap it in a type with more meaning than a opaque lambda (which I agree is a good thing).

edit: Full on nonsense just for laughs:

   auto user_age = [](auto&& user) noexcept (noexcept((user.age))) -> decltype(auto) requires requires { user.age;}  { return (user.age);  };


I think it may be possible to make it even more of a compile-time construct if uglier types were an option. Instead of holding a `Vec<PathComponent>` it could have been a list made out of nested types: `Path<First, Path<Second, Path<Third, Last>>>>`.


I had not heard of keypaths before. They seem to provide functionality similar to lenses in Haskell.


At least this implementation derives from NeXT's Enterprise Object Framework [1] created in 1994.

Which then made its way into Apple WebObjects, Cocoa and finally Swift.

[1] https://en.wikipedia.org/wiki/Enterprise_Objects_Framework


That's really cool I hadn't heard of them before!

For those that are interested, I found this article by FPComplete on the topic: https://www.fpcomplete.com/haskell/tutorial/lens/


Are keypath like Haskell lenses? First class composable getters/setters?

Or just the getter part?


Good article, thanks!

There is similar concept for json https://goessner.net/articles/JsonPath/

We heavily using it for validation error messages. Like

    {
      "error": "wrong value",
      "path": "foo/bar[1]/prop"
    }


JMESPath is what JSONPath should have been

https://jmespath.org/

specified, standardized, with tests and implementations for multiple languages


For JVM languages you have OGNL:

https://commons.apache.org/proper/commons-ognl


This is quite cool, I'm just having a hard time imagining when I would use it.


I hope I cover this somewhat in the 'motivations' section. Keypaths are used heavily in SwiftUI, and my interest in them came from working on trying to find ergonomic patterns for GUI libraries in Rust.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: