Dear Swift,
Of course you would forgive this missive if you only knew (as I do), that its author sorely lacks any semblance of expertise in the matter of its contents.
The whole thing is, in short, quite probably nonsense. Nevertheless, due to certain circumstances etc., here it arrives in your in-box, on what is most likely an otherwise quite pleasant day. Your indulgence is thus most humbly sought and appreciated.
It was soon after reading a recent dissertation upon the user interface of generic programming that I realised that something might be going on (to be clear, I mean in the dusty corners of my mind). I should stress that these goings on have little to do with generic programming or the writing of compilers, which I do not understand, as you shall soon plainly see. But it was in that context where certain matters became more apparent, and I began to wonder.
[ Now here is a matter of housekeeping: In the following paragraph I am going to mention the word 'sugar', but you must not take me amiss! I mean merely plain actual sugar, and nothing else. ]
It reminds me of salt and sugar, you see (there it is: merely plain actual sugar one must remember). Salt and sugar are so different and yet so similar all at the same time. When they are mixed together it is quite difficult to tell one from the other, and which bit is what. And then who can say which of the two is better? No-one! They are both of unquestionable importance, but each in its own separate way.
In particular it seems that we sometimes need more of the one, and at other times more of the other, and the upshot of it all is that one might prefer them to come, whenever sensible, in separate containers. With a pot in each hand we might enjoy a degree of control, or perhaps even a sense of clarity, which we might otherwise miss and pine for.
A small part of this is prompted by the width of things. This is one of the aspects which came to mind with the generics. It seems that computer programming, perhaps like poetry, generally follows a prosody of somewhat slender meter. Its sentences are brief, while its stanzas are long. It seems almost a matter of natural proportion for this manner of self-expression, and so something to be aimed for.
Another part of it is prompted by the interleaving. The interleaving makes two things which are quite clear on their own perhaps seem a little less clear when first one follows the other and then the other one follows that, followed closely again by the first one of all, and so on.
And then there is even the shape of things. We have a shape which we use, so it seems, for one lot of things, and then we don't use this shape for another, which makes this other thing seem so different, when it is perhaps not quite so different as we might think. Of course, this may be intentional, you will tell me so if it is, but I wonder nonetheless. I cannot help it.
Perhaps I can start with the shape.
Here we have a shape for a type of thing:
struct Thing {...}
It is very clear what type of thing it is. It is a Thing
. If this thing conforms to the type of another type of thing, we just say so:
protocol Essence {...}
struct Thing: Essence {...}
In this case the Thing
gains all the properties of Essence
, without really being an Essence
in itself.
If we so wish, it is possible, by taking a slightly different tack, for a Thing
to actually be an Essence unto itself, as well as also being its own Thing:
class Essence {...}
class Thing: Essence {...}
By either of these means we may construct types of Things
which can have all sorts of properties at once. Furthermore, again using this same basic shape, we can also construct other types of Things
which are less flamboyant, and limited to having only one property at a time:
enum Thing {...}
And this Thing can also have an Essence if we so wish:
enum Thing: Essence {...}
There is a chance I am mistaken (see above), but this is how I understand Things to be.
Except for this type of thing:
func ...
When we create a function, we do not follow the shape. We use this shape instead:
func thingCombinatorizer(a: Thing, b: Thing) -> Thing {...}
Here we see the interleaving. There is salt and there is sugar.
Now this is not a problem. It works very well and makes a lot of sense. But it nevertheless occurs to me that the shape seems different for this one certain kind of thing.
Also, there is the width. The thingCombinatorizer is not very wide (you will appreciate that I tried, by giving it a very long name, but it is still not very wide), but if it were designed to work on a more generic set of things, then it might start to get a little wide as well.
Now here is a thought. In some languages, the salt and the sugar are kept apart. I don't know about computer languages (see above), but there are other languages out and about that keep them apart.
Funnily enough, one of these languages seems to be Swift.
In Swift we can write a function which is like a song without words:
{ $0 + $1 }
Of course, this particular song has no meaning, because it has no context. But the chameleon nature of the song without words means that, in the right places, this might be sufficient to mean something.
In our situation, lacking context, we must provide the song with more meaning:
{ (a, b) in a + b }
This is the same thing, really, just written out. But despite the more abundant use of words there is one word in particular which this song still lacks, and that is a name. Without a name we cannot just freely refer to it whenever we want. It is anonymous.
let thingCombinatorizer = { (a, b) in a + b }
Now our function is no longer anonymous. We can now pin it down, and call it by name.
But we still lack meaning because of the salt and the sugar. At this stage, let's say we have the sugar, but we don't yet have the salt. In our func function, we had both the salt and the sugar, mixed in with each other. We can also mix the salt and sugar together here:
let thingCombinatorizer = { (a: Thing, b: Thing) -> Thing in a + b }
But ... intriguingly ... we can also separate the salt from the sugar if we prefer:
let thingCombinatorizer: (Thing, Thing) -> Thing = { (a, b) in a + b }
Now my understanding is that the difference between this 'anonymous' function and a 'real' func function is that the no-longer-anonymous one is defined using an expression instead of whatever the other way is called. But with a bit of license we can format that expression to look more like the other sort:
func thingCombinatorizer: (Thing, Thing) -> Thing {
(a, b) in a + b
}
Now here's the thing: Could we do this in Swift?
It occurs to me that someone might like to be able to do this. It seems potentially clarifying to be able to see the function types referred to without the arguments mixed in:
func compose: (B —> C, A —> B) —> (A) —> C
And it seems unlikely to be particularly objectionable, since it is possible to do this to some degree now in Swift, just not (so it seems) right where a proper func function is defined.
And currently not (it seems) (to me) (quite significantly) when using generics.
So here is what I think I mean:
Function declarations might in some alternate reality more closely follow the overall 'shape' of type declarations in general, bearing in mind that the type of each function is actually a quite real and useful thing to think about and understand:
As:
struct Person: Essence {
let property = CogitoErgoSum()
}
So:
func add: (Int, Int) -> Int {
(a, b) in a + b
}
The types of functions, and the arguments of functions would, in this alternate reality, be separated so that each can be seen clearly on its own.
There is of course precedence for this in mathematics at least, especially if one considers the delightful happenstance that:
f: A --> B
x |-> x + 1
Can be read either as an old-fashioned function f
with an old-fashioned domain of set A
and codomain of set B
, or as the new-fangled, type-theoretic notion of a term f
, of function type A --> B
, which ectomorphisms (or something) the argument x:A
to the value y:B
.
In particular, it occurs to me that this kind of dis-entangling of function types from their arguments could surface opportunities to express certain ideas in a clean-cut, one-thing-at-a-time manner.
For example, such a thing could, conceivably, improve the clarity of generic function declarations. Perhaps even the 'width' of function declarations (particularly generic declarations) might benefit from it.
The interleaving would of course be replaced by the need for a positional correspondence between argument names and their types. An opportunity might exist to design a syntax (aided possibly by some clever machine, such as a computer) to make this as clear and as simple as possible.
To extrapolate the idea and roll it about, take my first simple lunge at the notion. In this imaginary world, one could stick close to what one already knows and loves:
f: A --> B
{ x in x + 1 }
f: A --> B
{ x --> x + 1 }
This might result in themes and variations along the following lines:
func add: (Int, Int) -> Int {
(_ a, and b) in return a + b
}
func concatenate: <T>([T], [T]) -> [T] {
(a, b) --->
var result: [T] = []
result.append(a)
result.append(b)
return result
}
The next option springing giddily to mind is this, in which the arguments somehow extricate themselves from the clutches of their curly braces:
f: A --> B,
x { x + 1 }
f: A --> B:
x { x + 1 }
id est:
func add: (Int, Int) -> Int, (_ a, and b) { return a + b }
func concatenate: <T>([T], [T]) -> [T],
(a, b) {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
func add: (Int, Int) -> Int: (_ a, and b) { return a + b }
func concatenate: <T>([T], [T]) -> [T]:
(a, b) {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
For my next go, try this: It may (or may not) be possible (and/or desirable) to declare the name and arguments of a function as a prefix to a complete type annotation.
This would alter the mathematical convention noted above to match the following (sorry mathematicians, etc.):
f(x): A --> B
{ x + 1 }
This astonishingly naĂŻve approach might yield, exempli gratia, specimens of the following ilk. One notes the separation, and perhaps even the overall shape, of these declarations. Perhaps they are now a little more akin to other types of type declarations. I can only say that I think so myself, for whatever that is worth:
func add(_ a, and b): (Int, Int) -> Int {
return a + b
}
func concatenate(a, b): <T>([T], [T]) -> [T] {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
The call-site in this imaginary alternate reality would probably still offer the familiar interleaved template, because of the delightful ergonomics of tabbing into the proffered argument slots:
concatenate(a: [T], b: [T])
At the call site, anyway, it seems that the general fruderance of symbols is low compared to the point of declaration, where more abundant bejangles always seem necessary to get the job done.
So to put us back in the context of defining functions:
With separation, argument labels can be long, without affecting the type signature:
func thingCombinatorizer(byTakingAndUsing a, andCombiningItTruthmendouslyWith b): (Thing, Thing) -> Thing {
...
}
And versa-viso:
func add(a, b): (NumericalWonderProtocol, NumericalWonderProtocol) -> NumericalWonderProtocol {
...
}
One can always break lines where one wishes (subject to furious debate):
func combineThings(a, b):
(Thing, Thing) -> Thing
{ ... }
Annotations can go where they need to:
func changeThing(_ a): (inout Thing) -> () {...}
func serve(customer customerProvider) : (@autoclosure () -> String) -> ()
{...}
It might work with new generics:
func concatenate(a, b): (some Collection, some Collection) -> some Collection {...}
func concatenate(a, b):
<T>(some Collection<.Element == T>, some Collection<.Element == T>)
-> some Collection<.Element == T>
{...}
... and olden-day generics:
func concatenate(a, b): <T>([T], [T]) -> [T] {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
... and olden-day generics, with newen-day reverse-generics:
func evenValues(in collection):
<C: Collection> C -> <Output: Collection> Output
where C.Element == Int, Output.Element == Int
{
return collection.lazy.filter { $0 % 2 == 0 }
}
And it might even work with new-new generics (which you might not know about yet, because they are just a made up thing that I'm just making up):
func evenValues(in collection):
<C> -> <Output>
where
C == some Collection,
C.Element == Int,
Output == some Collection,
Output.Element == Int
{
return collection.lazy.filter { $0 % 2 == 0 }
}
func groupedValues(in collection):
<C> -> (even: <Output>, odd: <Output>)
where C == some Collection, C.Element == Int,
Output == some Collection, Output.Element == Int
{
return (even: collection.lazy.filter { $0 % 2 == 0 },
odd: collection.lazy.filter { $0 % 2 != 0 })
}
func concatenate(a, b):
([<T>], [<T>]) -> [<T>]
{
var result: [<T>] = []
result.append(a)
result.append(b)
return result
}
func repeat(element, numberOfTimes):
(<T>, Int) -> [<T>]
{...}
By the way, one of the nice things about new-new generics (the completely made-up one) is that one can organise one's thoughts to one's own personal satisfaction, whilst maintaining a crisp and elegant opening line:
func evenValues(in collection):
<Input> -> <Output>
where Input == some Collection, Output == some Collection,
Input.Element == Int, Output.Element == Int
{...}
func evenValues(in collection):
<Input> -> <Output>
where
Input == some Collection, Input.Element == Int
Output == some Collection, Output.Element == Int
{...}
And of course all the normal argument label things would apply:
func add(_ a, to b): (Int, Int) -> Int {...}
func add(_, _): (Int, Int) -> Int {...}
And so on and so forth.
But wait, here is another way it might be done! This one has the advantage of keeping any mathematicians e^happy
should one accidentally stray too far from Agda. Also, if you don't mind me saying, this one is heading into the territory of let poetry = e.e.cummings()
:
The mold:
A --> B
f(x) { x + 1 }
And the manifestation:
(Int, Int) -> Int
func add(_ a, and b) { return a + b }
<T>([T], [T]) -> [T]
func concatenate(a, b) {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
<C: Collection> (C) -> <O: Collection> O
where C.Element == Int, Output.Element == Int
func evenValues(in collection)
{
return collection.lazy.filter { $0 % 2 == 0 }
}
How sweet ...
Now, perhaps in a dream, one could overload by doing things like this:
(Int, Int) -> Int
(Int8, Int8) -> Int8
(Int16, Int16) -> Int16
(Int32, Int32) -> Int32
(Int64, Int64) -> Int64
(Double, Double) -> Double
(String, String) -> String
func combine(_ a, and b) { return a + b }
And of course in all the normal ways:
(Int, Int) -> Int
func add(_ a, and b) { return a + b }
(Int, Int, Int) -> Int
func add(_ a, and b, and c) { return a + b + c }
Long labels, without interleaving:
(Thing, Thing) -> Thing
func thingCombinatorizer(byTakingAndUsing a, andCombiningItTruthmendouslyWith b) {
...
}
Long types, without interleaving:
(NumericalWonderProtocol, NumericalWonderProtocol) -> NumericalWonderProtocol
func add(a, b) {
...
}
Annotations:
(inout Thing) -> ()
func changeThing(_ a) {...}
(@autoclosure () -> String) -> ()
func serve(customer customerProvider) {...}
New generics:
(some Collection, some Collection) -> some Collection
func concatenate(a, b) {...}
<T>(some Collection<.Element == T>, some Collection<.Element == T>)
-> some Collection<.Element == T>
func concatenate(a, b) {...}
Olden-day generics:
<T>([T], [T]) -> [T]
func concatenate(a, b) {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
Olden-day generics, with newen-day reverse-generics:
<C: Collection> C -> <O: Collection> Output
where C.Element == Int, Output.Element == Int
func evenValues(in collection) {
return collection.lazy.filter { $0 % 2 == 0 }
}
New-new generics (the made up ones):
<C> -> <Output>
where
C == some Collection,
C.Element == Int,
Output == some Collection,
Output.Element == Int
func evenValues(in collection)
{
return collection.lazy.filter { $0 % 2 == 0 }
}
<C> -> (even: <Output>, odd: <Output>)
where C == some Collection, C.Element == Int,
Output == some Collection, Output.Element == Int
func groupedValues(in collection)
{
return (even: collection.lazy.filter { $0 % 2 == 0 },
odd: collection.lazy.filter { $0 % 2 != 0 })
}
([<T>], [<T>]) -> [<T>]
func concatenate(a, b)
{
var result: [<T>] = []
result.append(a)
result.append(b)
return result
}
(<T>, Int) -> [<T>]
func repeat(element, numberOfTimes):
{...}
Label whatnots:
(Int, Int) -> Int
func add(_ a, to b) {...}
(Int, Int) -> Int
func add(_, _) {...}
If it's hard to parse or tokenize or demangle or shim or inline or statically analyse, or something like that, there might need to be some kind of things to wrap it up and make it how you need:
f: A --> B
f(x) { x + 1 }
functype add: (Int, Int) -> Int
func add(_ a, and b) { return a + b }
functype evenValues: <Input> -> <Output>
where Input == some Collection, Output == some Collection,
Input.Element == Int, Output.Element == Int
func evenValues(in collection)
{...}
A --> B
f(x) { x + 1 }
functype (Int, Int) -> Int
func add(_ a, and b) { return a + b }
functype
<T>([T], [T]) -> [T]
func concatenate(a, b) {
var result: [T] = []
result.append(a)
result.append(b)
return result
}
Please don't laugh.
Anyway, I have to be off, so cheerio – and I wish you a lovely day.