www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Lenses-like in D

In my opinion it's interesting to look at other languages. Often 
in functional languages you have immutable records, that 
sometimes contain other inner immutable records. If you need to 
"change" fields, you usually create a copy of the record with 
just one modified field. To do this with a handy syntax they use 
"lenses" in Haskell in Scala and other languages.

See, regarding Scala:


So you have a get and set methods, where set returns a different 
record and leaves the original record unchanged. An example usage 
in Scala:

case class Address(city: String)
case class Person(name: String, address: Address)

val yankee = Person("John", Address("NYC"))
val mounty = Person.address.city.set(yankee, "Montreal")
Person.address.city.get(mounty) // == "Montreal"
val cityLens: scalaz.Lens[Person, String] = Person.address.city

As immutable structs/tuples become more common in D code, I think 
it's handy to have something similar in Phobos. Maybe just the 
"set" is enough for now. This is a start of a D implementation:

import std.stdio, std.string, std.traits, std.array, 

immutable struct Address { string city; }
immutable struct Person { string name; Address address; }

private bool withFieldVerify(string path, Data, Field)() {
     enum pathParts = path.replace(".", " ").split();
     if (path.length < 1)
         return false;
     mixin("alias TField = " ~ Data.stringof ~ '.' ~ 
pathParts.join(".") ~ ";");
     return is(Unqual!(typeof(TField)) == Unqual!Field);

private string genReplacer(string path, Data, Field)() {
     enum pathParts = path.replace(".", " ").split();
     string[] replacer;
     foreach (name; __traits(allMembers, Data))
         replacer ~= (name == pathParts[0]) ? "newField" : ("p." ~ 
     return replacer.join(", ");

Data withField(string path, Data, Field)(Data p, Field newField)
if (is(Data == struct)
     && !__traits(hasMember, Data, "__ctor")
     && withFieldVerify!(path, Data, Field)()) {
     return mixin("Data(" ~ genReplacer!(path, Data, Field)() ~ 

void main() {
     auto yankee = Person("John", Address("NYC"));
     auto foo = yankee.withField!q{name}("Foo");

     //auto mounty = yankee.withField!q{address.city}("Montreal");

     // To be improved: this gives errors inside setFieldVerify:
     //auto mounty = yankee.withField!q{address foo}("Montreal");

//    assert(mounty.address.city == "Montreal");
//    alias withCity = withField!q{address.city}; // shortcut
//    auto mounty2 = yankee.withCity("NYC");

- Lenses are meant to "update" only one field, at arbitrary 
nesting level.
- This code is meant to work only on struct/tuple instances, the 
struct can't have explicit costructors.
- The code should be improved so it avoids to generate error 
messages inside setFieldVerify.
- withField/genReplacer probably have to become recursive, so 
withField becomes able to "update" nested fields like 
- withField() is probably meant to be usable on mutable struct 
instances too, but it's much more useful on immutable ones.
- I think in D you can't enforce a class to have a dumb 
constructor (dumb means it just copies its input arguments into 
instance fields with the same type), so withField() can't be used 
on classes.
- Only withField is public, the other names are module-private. I 
think this makes its usage simple. The usage syntax of withField 
is not wonderful, but I think it's acceptable.
- All this is far from being the nice composable lenses of 
- Adding a related higher-order function that performs like this 
"alter" is possible, it takes another function in input and 
returns the record with the given function applied on the desired 

Nov 10 2012