digitalmars.D - Lenses-like in D
- bearophile (91/91) Nov 10 2012 In my opinion it's interesting to look at other languages. Often
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
Nov 10 2012