Where to start if you want to extend `Any`

Hello there, I would like to implement (or at least learn and try) a proof of concept implementation of the with function from this thread:

[Pitch] Circling back to with

Disclaimer: I have no experience in compiler development (I only read this tutorial a few years ago: Kaleidoscope Tutorial — LLVM 16.0.0git documentation) and my C++ knowledge is at it's very basics. However I'm willing to learn more and build things the way they should be build. That said, please bear with me.

My personal goals (click to unfold)
  • Setup the Swift project on my local computer and make it compile first

  • Make a small change somewhere and see if it's still compiles

  • Discard that test

  • Search some code which already extends Any and to learn from it

    • If I remember correctly KeyPath does it somewhere according to the original proposal, but I couldn't find the code yet. If someone can pin-point me to a simple function on Any that would be great. (cc. @Joe_Groff )
  • Extend Any with a simple small function like func fooOnAny() { print("does it work?") } to test whether or not the implementation worked

  • Reverting the simple function and tackling the with function

  • Writing tests and documentation for the with function

  • If everything goes well providing the implementation for the proposal and tackle the review


Furthermore I would love to document my learning curve here so other people starting with their first contribution like me can benefit from it and don't make the same mistakes I might run into during the whole process.


Constructive feedback is always much appreciated.

Best regards,
Adrian

3 Likes

Where the keypath proposal says "effectively add the following subscripts" and then shows the example with extension Any, it really means "hack the compiler to make it look like everything has these subscripts". It's not extending Any in a normal sense.

All of magic to make this happen is in the expression type checker (see CSGen.cpp, CSApply.cpp).

I know it might be diverting the topic a bit, but is there any desire to actually allow extending Any? If not, is it for a design choice or (temporary?) technical issues?

1 Like

During early Swift days (pre-open source) I asked for extending AnyObject as a replacement for categories on NSObject. The answer was they did not want to do this, not only for performance reasons, but because it would be a bad design and would lead to bad code and unexpected dependancies and assumptions. From the other discussion about removing NSObjectProtocol, it is evident that implicit assumptions about what methods are available on NSObject is showing its ugly head even in Apple's own core frameworks such as Foundation. Imagine what could happen if anybody could extend Any and add methods to every type everywhere.

That is what I thought after I couldn't find any extension Any with some kind of extra internal attributes that would convince the compiler to allow the extension.

I already had a quick glance at the files and tried to filter those by KeyPath keyword, but it is still quite overwhelming for the first time. Let's see if I'm able to find the very first entry point I need on my own. Thank you for sharing the file names.

Did you meant it in a general way? If so, I don't really remember anymore. I think there was some discussion and extending Any may become possible in a far future, but I also could be completely wrong on that. I only though that KeyPath seems to extend Any (even if it's a hack as noted by Mark), we could try to implement the with function as an extension on Any as well, which would play nicely with optional chaining. That said, this is a more elegant and flexible design choice in my opinion.

I'm not quite sure if you were replying to the original post or to Davide's post. I do understand the points you're making, yet I wouldn't want to just throw away the idea of a with function which is available to use everywhere. I would prefer two separate extensions though, one on AnyObject and the second one on the non-existing AnyValue. Since the latter existential does not exist Any is the only way to go. While it might be true that extending Any isn't something the core team would want to allow, a proposal still requires some implementation and the review can still reject everything if it's really a no go.

That said, I would really appreciate if someone could help me out from time to time even if extending Any is a grey area topic.

I was replying to Davide’s post.

1 Like

Have you considered defining an operator for this instead?

It looks like there might be some type checker issues we would have to work through (see SR-7171), but this approach may be something that could be made to work.

precedencegroup WithPrecedence {
  associativity: left
  higherThan: BitwiseShiftPrecedence
}

infix operator .* : WithPrecedence

func .* <T>(lhs: T, rhs: (inout T) throws -> ()) rethrows -> T {
  var l = lhs
  try rhs(&l)
  return l
}

struct S {
  var i: Int
  var j: Float
}

let s = S(i: 7, j: 0)
let t = s.*{ $0.i = 1 }.*{ $0.j = 2 }

print(s.i)
print(s.j)

print(t.i)
print(t.j)
1 Like

I use something like this:

infix operator +=+

func +=+ (lhs: T, rhs: (inout T) -> ()) -> Void {

rhs(&lhs)

}

var player: ... Create a SpriteKit node ...

player.physicsBody +=+ { it in

it.rotationalVelocity = 2.1

it.zRotation = 3.0

...etc...

}

To be honest I never thought about an infix operator. Even if I personally would prefer .with for readability reasons I'm quite surprised that I never tried such approach already. In my code base I use a protocol which I conform all of my types to that requires that functionality.

Maybe we could also introduce a left assignment operator? That way this feature would be completely opt-in and not another .dynamicType thing.

However I wonder if we can make it work with optional chaining, is it a bug is the precedence not correct here?

precedencegroup LeftAssignmentPrecedence {
  associativity: left
  higherThan: BitwiseShiftPrecedence
}

infix operator <- : LeftAssignmentPrecedence

@discardableResult
public func <- <T>(lhs: T, rhs: (inout T) throws -> ()) rethrows -> T {
  var value = lhs
  try rhs(&value)
  return value
}

struct S {
  var i: Int
  var j: Float
}

let a = S(i: 7, j: 0)
let b = a <- { $0.i = 1 }

let test: (inout S) -> () = { $0.j = 2 }

let c: S? = b
let d = c? <- test // error: optional chain has no effect, expression already produces 'S?'
let e = b <- test

@Erica_Sadun what do you think about an infix operator instead?

2 Likes

You can make it work with optional chaining by making it an "assignment" operator:

precedencegroup LeftAssignmentPrecedence {
  associativity: left
  higherThan: BitwiseShiftPrecedence
  assignment: true
}

Though this feels like a bit of a hack as the operator doesn't actually perform any mutation of the left operand.

1 Like

That is still neat. Thank you for telling me that.

Is the issue you reported also related to this (playground cannot execute it)?

func foo(_: Int) {
  print("test")
}

print("1")

foo .* {
  $0(42)
}

print("2")