Is there anything about Swift worth emulating [re lenses]?

I’m curious whether those at the helm of Scala see anything in Swift worth emulating.

I’ve been using Swift for a while now, and mostly I miss Scala, but one thing that I find interesting about Swift is how their immutable struct semantics give you much of the convenience of lenses with zero effort. For example, if I define these structs with var members:

struct A { var b: B }
struct B { var c: C }
struct C { var d: Int }

I can modify their values, but the new values are set in automatically created copies of the originals, which remain unchanged:

let a1 = A(b: B(c: C(d: 3)))
var a2 = a1 // just an alias
a2.b.c.d = 4 // copies are made of all the structs
print(a1.b.c.d) // 3 (a1 is immutable)
print(a2.b.c.d) // 4 (also immutable, but with different values)

You can also define a “mutating func” on a struct that can modify several member vars at once, and the same sort of copy-on-write behavior is invoked.

I’m curious what folks think of this.

Nicholas

3 Likes

I can see where zero effort lenses are desirable, but this syntax looks very counter-intuitive to me. I think it’s better to be a bit more explicit about where things get mutated and where things get copied.

1 Like

What syntax in particular strikes you as being very counter-intuitive, Jasper? Or do you just mean that in the line

the copy-on-write semantics are surprising? It is true that in Swift you need to be aware of whether you are working with a class or a struct, because they behave differently.

Nicholas

Yes. It looks like you’re assigning something to d but you’re actually copying and reassigning something to a2.

Using a different operator than = might be better. To make it more explicit at the use site.

1 Like

I would find this extremely useful. copy is clever, but doesn’t scale to deep modifications. I don’t bother with optics, because I have to write so much boilerplate before I can use them, and even then, the syntax is clunky. I agree with @Jasper-M though that different semantics should have their own syntax. It’s worth thinking about what this might look like, and how it might desugar.

This would especially useful on top of https://github.com/lampepfl/dotty/issues/1970.

I have a dream of Scala one day meeting its true potential as a language for compactly defining and manipulating structured data. So many of the pieces exist, but just aren’t integrated out of the box, and with dedicated syntax. This is one of the things that makes approaching Scala projects intimidating to beginners, in my experience.

1 Like

I should clarify that the compiler actually does in a sense force you to be aware that you are not mutating the original struct. The following code fails to compile:

let a1 = A(b: B(c: C(d: 3)))
let a2 = a1 // this time ‘let’ instead of ‘var’, so immutable
a2.b.c.d = 4 // ERROR: a2 is not a var

By telling you that a2 is not a var, the compiler is reminding you that it has to make a copy of the D struct in order to do the assignment, and for that it will need to create a new C, B, and A as well. Variable a2 will need to be changed to the new A’s address, so it can’t be a ‘let’ variable.

It may seem strange if you aren’t used to it, but as a Swift user I find it intuitive.

The one thing I don’t like about it is that as a var a2 is open to further modification. I would prefer to be able to do something like this:

let a1 = A(b: B(c: C(d: 3)))
let a2 = a1 but {
$0.b.c.d = 4 // $0 refers to the copy of A
}

so that a2 is still immutable.

Or I suppose you could go ahead and use a2 instead of $0:

let a1 = A(b: B(c: C(d: 3)))
let a2 = a1 but {
a2.b.c.d = 4
}

I thought it was worth seeing how hard it is to add such functionality in a library. I came up with a simple macro-based proof of concept of ~50 lines including white space: https://github.com/Jasper-M/simple-lenses.

It’s only intended to work with case classes, and probably full of potential bugs (or potential opportunities for improvement).

scala> import simplelenses.implicits._
import simplelenses.implicits._

scala> case class A(b: B); case class B(c: C); case class C(d: Int)
defined class A
defined class B
defined class C

scala> val a1 = A(B(C(3)))
a1: A = A(B(C(3)))

scala> val a2 = a1 but { _.b.c.d <~ 4 }
a2: A = A(B(C(4)))

The above is transformed into a1.copy(b = a1.b.copy(c = a1.b.c.copy(d = 4))) (or at least something that could be inlined to that with some extra effort).

2 Likes

Besides all the lens libraries, see https://github.com/kenbot/goggles

1 Like

Because many don’t click on links and because Goggles looks so cool, let me paste their pitch:

import goggles._ 

case class Topping(cherries: Int)
case class Cake(toppings: List[Topping])
case class Bakery(cakes: List[Cake])

val myBakery = Bakery(List(Cake(List(Topping(0), Topping(3))), 
                           Cake(List(Topping(4))), 
                           Cake(Nil)))

get"$myBakery.cakes*.toppings[0].cherries"
// List(0, 4)

set"$myBakery.cakes*.toppings[0].cherries" := 7
// Bakery(List(Cake(List(Topping(7), Topping(3))), 
//             Cake(List(Topping(7))), 
//             Cake(Nil)))

The DSL runs in the compiler, and is completely typesafe. It generates plain Monocle code.

Their readme also compares to other libraries in the space (like a related work section of a good paper).

So let me relaunch: is there any lens library that is good enough to be a part of the Scala Platform? I’ve always heard that lenses in Scala are too inconvenient, but this thread (and Goggles) suggests there might be hope…