Lenses-like in D
bearophile
bearophileHUGS at lycos.com
Sat Nov 10 11:12:40 PST 2012
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:
http://blog.stackmob.com/2012/02/an-introduction-to-lenses-in-scalaz/
https://github.com/gseitz/Lensed
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,
std.typetuple;
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." ~
name);
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");
writeln(foo);
//auto mounty = yankee.withField!q{address.city}("Montreal");
//writeln(mounty);
// 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");
}
Notes:
- 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
yankee.address.city.
- 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
Haskell:
http://www.haskellforall.com/2012/01/haskell-for-mainstream-programmers_28.html
- 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
field:
http://hackage.haskell.org/packages/archive/lenses/0.1.2/doc/html/Data-Lenses.html#v%3Aalter
Bye,
bearophile
More information about the Digitalmars-d
mailing list