TysonMN / tysonmn.github.io

Blog of Tyson Williams about software engineering and functional programming
https://tysonwilliams.coding.blog/
MIT License
3 stars 7 forks source link

2021-01-30_function_composition_syntax #26

Closed utterances-bot closed 3 years ago

utterances-bot commented 3 years ago

Function composition syntax

https://tysonwilliams.coding.blog/2021-01-30_function_composition_syntax

cmeeren commented 3 years ago

I find the backwards composition operator useful when composing a predicate with not. For example, I find List.filter (not << isEmpty) more readable than List.filter (isEmpty >> not). Other than that, I rarely use it (if ever).

TysonMN commented 3 years ago

That is a good use of the backwards composition operator.

At the same time, I think it is compensating for a previous suboptimal choice. I think the word "filter" does clearly convey its behavior. Consider these definitions of the verb form.

The emphasis here is negative. Both definitions are "negative" in the sense that the word "remove" is negative. I did not cherry-pick these definitions either. All other alternative definitions for the verb form were neutral. I could not find any definition with a "position" sense to it. The noun form of filter is definitely negative. The filter keeps the bad part and lets the good part through. When you look at a filter, you see the bad part and will eventually throw it away with the filter.

The top three synonyms from Google Search via Oxford Languages of the verb form of filter (when I search, in case that matters) are sieve, strain, and sift. The definitions for the first two (sieve and strain) are positive; i.e. sieving for gold and straining pasta via a strainer. That last (sift) is more ambiguous. One can sift flour to "remove lumps or large particles" or generally "examine (something) thoroughly so as to isolate that which is most important."

Let's return to programing. When someone sees List.filter predicate for the first time, do they think the resulting list contains the items for which the predicate returns true or false? I know that answer, and I still have to think twice about the correct answer every time I see such a line of code.

English certainly prefers "not is empty" over "is empty not", but it prefers "is nonempty" stronger still.

I think I prefer to move the inclusion or exclusion to the function name (i.e. to the verb). I think it might be better to use List.keep predicate and List.remove predicate instead of List.filter predicate and List.filter (not << predicate) respectively. However, I have never tried this yet. I have never written such code. I have only been thinking (a lot) about it.

cmeeren commented 3 years ago

The top three synonyms ...

Another standard name for filter is where, c.f. the LINQ extension method Where. List.where is even defined in FSharp.Core as an alias for List.filter.

TysonMN commented 3 years ago

where is much clearer than filter, but it still doesn't have an antonym. Contrast this with Option that has both isSome and isNone. Nobody writes Option.isSome (not << predicate), so why should any of us write List.filter (not << preciate) or List.where (not << preciate)? It is because filter and where do not have antonyms.

Furthermore, I prefer to minimize the use of parentheses (because I am not a finite state automata with a stack that I can push to and pop from), so List.remove predicate is also better than List.filter (not << predicate) because it has fewer parentheses.

cmeeren commented 3 years ago

Part of me agrees with you. Perhaps the major part. But I have been indoctrinated with the guideline that there should generally not be both "positive" and "negative" versions of the same functionality (like isSome and isNone), which is hard to shake off. Not sure where I have it from, but likely some .NET API design guidelines I read long ago.

There are good reasons for this. Negation is just a not away, and you don't have to learn twice the number of terms to understand an API. It's more composable. But on the opposite side, something can definitely be said for readability.

I don't think I would define List.keep and List.remove myself and use them, but I do sometimes define e.g. isPrivate and isPublic for a domain type that can be in one of the two states. The domain rules become more readable when both of these are available.

seankearon commented 3 years ago

I think that I now understand why I have never managed to understand or properly weild the functional composition operator in F#. That is, because >> is postfix!

For me, composing two functions was always done in the same manner I was taught when I was a maths student (quite a long time ago!). What was in the prefix manner:

"g ∘ f " to mean "first apply f, then apply g"

That is: ⬜⚪(s) = ⬜(⚪(s))

I cannot remember encountering the use of functional composition (in maths) in the postfix manner. Reading up in Wikipedia (https://en.wikipedia.org/wiki/Function_composition), they do mention the alternative style.

I was also not aware of the reverse composition operator << until today and I have therefore avoided composing functions in my F# code. The use described by @cmeeren for negation of a function (List.filter (not << isEmpty)) is a common place where I mutter to my myself about wishing I understood how to use composition there, but end up either piping or taking the inverse function approach he later mentions (isSome and isNone). I may now try to sprinkle a little bit of reverse pipe into the mix! :)

For my two cents worth on the topic, I greatly perfer the prefix syntax. Probably simply because it is how I was taught about functional composition. I was taught that when I was studying Mathematics and I have never considered that perhaps folks from other disciplines may have approached learning functional composition using a postfix style. For me, to avoid surprises, I'm going to stick to the prefix style.

Thank you, Tyson, you've completely spun my head around and then straightened it back out again!

TysonMN commented 3 years ago

Hello @seankearon. Thanks for writing :)

I am happy to have introduced you to the backwards composition operator. It definitely has its uses.

The advantage of the prefix style (as you call it) is its similarity to some areas of mathematics. For example, in my limited study of category theory, I have only seen this prefix style for function composition. I can't remember now where I saw function composition defined the other way around. Maybe it was it in theoretical computer science where I spent most of my time.

I will keep using the forward pipe operator and forward composition operator so that order of execution matches the order in which I read the code, which is left to right.

seankearon commented 3 years ago

After writing, I was feeding the animals and suddenly realised that f >> g has the same effect as piping f |> g. So, now I understand it better I'm probably going to be using >> a lot more now. Thanks again, @TysonMN :bulb: :eyes: :rocket:

TysonMN commented 3 years ago

Indeed. The following are equivalent.

x |> f |> g
x |> (f >> g)

When the dust settles, I prefer the top over the bottom, but the second one is very useful when refactoring. Here is an example of me using that refactoring step.

I am happy that you will be using >> more often :D

cmeeren commented 3 years ago

I think it might be better to use List.keep predicate and List.remove predicate

Another thing that just struck me: These phrases work well for lists and other types that are clearly "containers" of multiple elements. They do not work as well for other traversables(?). For example, they still work, but not as intuitively, for single-element containers such as Option. But consider Gen in Hedgehog. It also has Gen.filter (and is a "single-element container" as Option), but intuitively to me, it's even more weird than with Option to think of Gen as "keeping" or "removing" an element.

TysonMN commented 3 years ago

(Serious question) What do you "intuitively" think it means to filter a Gen<_>?

cmeeren commented 3 years ago

Hm. When I see Gen.filter predicate I think "re-generate if the predicate is false". In other words, what my mind reads is Gen.regenerateUnless. Though that feels weird now that I type it out, so I'm not entirely sure.

TysonMN commented 3 years ago

I cannot remember encountering the use of functional composition (in maths) in the postfix manner. Reading up in Wikipedia (https://en.wikipedia.org/wiki/Function_composition), they do mention the alternative style.

Yes, some discussion of the alternative style is given in this section.

@seankearon, I just happened upon Wikipedia's article for inverse semigroup, which says

The convention followed in this article will be that of writing a function on the right of its argument, e.g. x f rather than f(x), and composing functions from left to right—a convention often observed in semigroup theory.

I just wanted to enrich this conversation with this reference about a specific community using the "alternative notation".

seankearon commented 3 years ago

Ah, that is interesting. I wonder what it is about the study of semigroups that is better when using function-last composition notation. I don't recall studying semigroups as an undergraduate, so I'm guessing that this would be a post-graduate level subject?

TysonMN commented 3 years ago

I wonder what it is about the study of semigroups that is better when using function-last composition notation.

Even though the whole point of my post is to argue in support of the claim that postfix notation is better, I don't think it is "significanltly" better. It is much closer to "50-50" than to "0-100". From that perspective, I think the most likely explanation is that each subfield somewhat randomly selected which notation to use. This is somewhat similar to driving on the right or left side of the road in different countries. Sure there are correlations between countries sharing a boarder or with similar cultures, but there is also a lot of randomness that lead to the current usage pattern.

seankearon commented 3 years ago

I really like the analogy of which side of the road you drive on. You certainly don't want to be mixing your composition syntax choices in closely related contexts! I'd be happy to believe that you're right about these choices being somewhat random. :)

dwincort commented 3 years ago

The situation for Haskell is a little more complicated than you described due to lazy evaluation -- in fact, . is already in left-to-right order for evaluation. Consider the composition of const 3 and myComplicatedFunction. If you write: const 3 . myComplicatedFunction $ undefined, well, reading from left-to-right, you know right away what the result is. You must first evaluate const, and you'll quickly realize that you don't need to evaluate the second argument. Writing this as undefined & myComplicatedFunction >>> const 3 forces you to read through backwards to get the same conclusion.

This may seem like a degenerate example, but the idea does happen. For instance, it's not that uncommon to produce an infinite list via a function and then want to compose with something like take 10. I find putting take 10 first (i.e., on the left) makes clear what the result will look like, while putting it last would force the reader to read everything else to see that the resulting list has ten elements.

In total, I see it as the difference between push and pull. Do you see evaluation as pushing values through functions (in which case you may want to use >>>), or as pulling results out of functions (in which case the traditional . makes more sense)?

TysonMN commented 3 years ago

Hello @dwincort. Thanks for writing.

I agree that laziness has the potential to change my thoughts and conclusions. I intentionally avoided discussing laziness and somewhat implicitly assumed eagerness. In particular, I am sympathetic to your argument about take 10. The phrase "take 10 integers" is more idiomatic for English than "integers take 10".

Your distinction of push vs pull is interesting I do think of computation as pushing values through functions, which could explain why I prefer the >>> operator in Haskell.

TysonMN commented 1 year ago

This article called Left to right, top to bottom makes argument that is similar to mine.