Mumble, mumble, functions

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.

9 Likes

This sugar is not sweet enough. How would you add labels to a multiple return type?

This was quite a long exploration, so I admit I didn't quite read it in full, but without commenting too much on what things would look like today, I'd like to link my post about why Swift moved away from uniform treatment of functions as A -> B entities (and more specifically why parameter lists are not tuples).

4 Likes

Roast, grind, brew, decant
Blue mug, one spoon of sugar
Dammit, that was salt

1 Like

With apologies, I can see that certain tendencies of mine may have gotten a little in the way.

To be clear, I am merely wondering whether one thing written as:

f(x: X, y: Y) --> (n: N, m: M) {...}

Could sensibly be written in two parts (still the same one thing underneath, and otherwise identical in every way):

f(x, y) --> (n, m) :
X, Y --> N, M
{...}

And whether such a thing might be useful or interesting in some way.

In my imagination, I empictured these parts somehow being put back together to match the other way, so that they might become one and the same thing underneath.

It appears from what Jordan has said (thank you Jordan), that it may not be possible or sensible to do such a thing (possibly because the 'argument' part looks like it is made of tuples?).

If that is indeed the case, then one must say 'Oh, well, nevermind', and move along. I do feel wiser for the exercise, thank you.

What I take away from this is that you're thinking that instead of writing:

func add(_ x: Int, to y: Int) -> Int { ... }

It might be better structured with the types separate from the names, along the lines of:

func add(_ x, to y): (Int, Int) -> Int { ... }

Leaving aside the practical considerations of whether we should make changes this large in a mature language, I still don't think this would be a good idea because it separates two pieces of information—parameter names and types—that are rightly coupled. That means:

  • It's more difficult to figure out the type of a given parameter—you need to figure out the offset into one list and then find the corresponding element in the other.
  • It creates two sources for the function's arity, which means those two places could disagree.
  • It means you need to edit in two separate places when you want to add, remove, or rearrange parameters.

In general, it just introduces a lot of new opportunities for a programmer to make a mistake. There's a nice conceptual elegance to it, but I think in practice it wouldn't work out very well.

10 Likes

@beccadax This somehow reminds me of step one in the SE-0111 commentary. Your example would be:

let `add(_:to:)`: (Int, Int) -> Int = { $0 + $1 }

add(1, to: 2) // $R0: Int = 3

Thank you Brent, you are of course correct in every way.

I see that in seeking to more clearly articulate the type of the function, one must sacrifice too much with respect to managing the types of its elements.

My misplaced enthusiasm arose principally from the observation that this scheme is already to a limited extent expressible using anonymous functions (the connection which Ben has helpfully clarified with respect to previous ideas along these lines), in conjunction perhaps with my own habit of outlining the type of many non-trivial functions within comments placed above.

I was also captivated by the notion that such a thing might make describing generic functions more 'straightforward', in the sense that such work seems much about manipulating expressions of type variables, which could conceivably be simpler if the element variables were in a manner of speaking 'out of the way'.

Thank you both for your thoughtful comments, they are most appreciated.

Or the latest related thread about compound names:

2 Likes

It does appear that 'step one' which Chris outlined in 2016 is very much the thing I wished to convey, but there formulated specifically in the context of restoring argument labels to anonymous functions (when declaring a variable or property), rather than as a shape which might be employed for a more general purpose.

Indeed, 'step two' of that same commentary suggests that this shape might be subsequently coated to match 'interleaved' function declaration syntax, which is noted as being 'nice declaration syntax', for reasons that I do sincerely quite agree with and understand.

Since this process, from step one to step two, is the reverse of the idea under discussion, I suspect that the general notion of 'separation' in function declarations may not have much in the way of traction. Perhaps Swift currently having both 'interleaved' and 'separated' forms of anonymous function declarations is an artifact of implementation rather than a matter of design. Perhaps also the design of functions and anonymous functions is more significantly different underneath than I might have supposed (since at face value they both encode the notion of a 'function').

With these points in mind, it seems less likely that a coating going the other way would be deemed desirable as an option for 'proper' functions, even if such a thing were to prove feasible (I realise that such a thing would also need to accommodate generics and other complications).

On this last point I feel the need to clarify (to the world at large), as I have a funny feeling that I may have been a little vague. Specifically, I hope it has not come across that I thought 'separated' declarations should be considered instead of the current 'interleaved' formulation. Goodness, no! It was my intention only to speculate upon the possibility of an additional option, an alternative mode of expression if you will. I really did not intend to give any wrong impression in this regard.

Anyway, thank you for pointing out these previous discussions. It never occurred to me that these things might be connected, and I have learned much in the process.