[swift-evolution] Idea for enabling DSLs: bind to self in closures

David Waite david at alkaline-solutions.com
Sat Dec 5 14:46:22 CST 2015


> On Dec 4, 2015, at 4:49 PM, Kevin Ballard <kevin at sb.org> wrote:
> 
> On Fri, Dec 4, 2015, at 03:32 PM, David Waite wrote:
>> A few thoughts:
>> 
>> 1. In a lot of situations they are not pure functions - they have state
>> associated across them determined by the context in which your closure
>> was called. So the import would not be of a static function, but of an
>> input parameter, aka:
>> 	it(“…”) {
>> 		builder in
>> 		import builder
>> 		expect(sections).to{…}
>>         }
>> 
>> Assuming expect is the only function, this may very well be equivalent to
>> 
>> 	it(“…”) {
>> 		builder in
>> 		let expect = builder.expect
>> 		expect(sections).to{…}
>>         }
> 
> I don't think we want to add `import builder`, importing methods that
> are implicitly bound to some value seems like a dangerous can of worms
> to open up.

I’m not sure about that; isn’t that exactly what ‘self’ is? If anything, it is deciding whether it is worth having two cans of worms open. And the semantics would likely be similar to self - it has to be a fixed value type or reference through the scope.

Import might be a poor overloading of an existing concept though. Alternative syntax based on the setup closure thread’s ‘with’ syntax examples:

	it(“…”) {
		builder in
		with builder {
			expect(sections).to{…}
		}
        }

but obviously I would like to not have the extra level of nesting, so probably more like:
	it(“…”) {
		with builder in
		expect(sections).to{…}
        }

Using .. syntax likely would be inappropriate for this example, since there may be additional business logic in between calls to expect.

> If you need state (and don't want to encode that state as
> thread-local variables, though of course Swift doesn't currently have
> support for those outside of NSThread.threadDictionary), then personally
> I don't think it's a big deal to require the explicit state argument.
> You could even adopt a convention of using $ for the identifier here
> (while $0, $1, etc are defined by the language, $ appears to be open for
> use as an identifier), so that would look like
> 
> describe("foo") { $ in
>    $.it("has bar") { $ in
>        $.expect(sections).to(...)
>    }
> }

This is probably the simplest alternative to my proposal, and requires no language changes. However,
- IMHO the $0, $1, etc syntax is meant for when terseness is a benefit that outweighs readability. This is primarily because $ looks more like an operator than part of a parameter name, and the names themselves aren’t based on the signature of the method calling the closure. Even after a fair amount of swift work, I stumble whenever I see $0, etc syntax. For this reason, using $ as the parameter name feels like it counteracts the expressiveness I was going for.
- Coming up with an alternative explicit name (builder? context?) for a passed parameter is hard, because the code is often not so much manipulating that state as it is operating within the context of that state. The term the language gives us for this is ‘self’, but that isn’t assignable/overridable. This could actually wind up making $ feel more like a keyword than an arbitrary parameter name choice.

> 
> And you can also do things like make the expectations actually be static
> members of their return value, so you'd have code like
> 
>    $.expect(sections).to(.contain(bar))

I hadn’t considered that - your Swift-fu is strong :-)

> In the future, if Swift ever gains a fully-fledged macro system, then
> maybe you'd be able to rewrite these scopes-with-state as macros that
> carry implicit state. Or, heck, maybe someday we'll have higher-order
> types AND a monadic system (and either custom syntax or macros to
> simulate Haskell's `do` notation) and then we can just use monads to
> represent the state :D
One could hope - although I don’t believe macro systems or monads/monoids as a concept (rather than an API influence like Optional) are conducive for learning languages.

>> 5. Imports likely should generate conflicts at compile-time if they
>> shadow defined functions if you can do wildcard imports. No need to have
>> syntax to alias names - one should either change the code to not conflict
>> or use the longer-form names
> 
> I agree that imports should throw an error if they'd shadow something
> defined locally (though perhaps they can still shadow things from other
> imports?). But I would actually like an alias syntax so you can import
> something under a different name, even if it's just restricted to naming
> modules in some kind of `import qualified` syntax (that would require
> the module name to use any member), e.g. `import qualified
> LongModuleName as L; /* ... */ L.foo(bar)`.

I was thinking more that "import LongModuleName.foo as bar” perhaps would be abused.

>> 6. import could be an attribute:
>> 	it(“…”) {
>> 		@import builder in
>> 		expect(sections).to{…}
>>         }
> 
> What would that actually be an attribute of? Attributes aren't distinct
> items, they modify something else. And why make this an attribute when
> we have a perfectly good `import` keyword already?

Yeah, drop that idea.

-DW


More information about the swift-evolution mailing list