Falling foul of the "|>" operator with a non-commutative function


#1

Hi,

Just thought I’d share something that I was just caught out by, I was trying to combine a series of functions on some data and used a non-commutative function which completely changed the meaning in a way that took me some time to track down. I was doing the following:

let s = Vec2d.create x y  
     |> Vec2d.sub offset
     |> Vec2d.scale d 
     |> Vec2d.add subsample 

Here I’ve got a 2d vector from which I want to subtract another vector, scale it by a scalar value and add another vector back on to. This mostly works but since Vec2d.sub is non-commutative what it actually does is subtract the created vector from the offset. Since the dominating value is actually the x and y values not the offset this had the most notable effect of inverting the axis!

Anyway thought I’d share to alert any other learners that they should be careful when using this operator with non-commutative functions.

Also if anyone has a good suggestion for general workarounds to this, I would love to hear them. What I’ve done is create a sub_by function which doesn’t feel very satisfactory and adds an unnecessary function to my Vec2d module.


#2

Actually I just found in the Core.Std library that there is a function called Fn.flip that seems to do what I’m looking for, allowing me to use non-commutative functions without too much bother:

let s = Vec2d.create x y  
     |> Fn.flip Vec2d.sub offset
     |> Vec2d.scale d 
     |> Vec2d.add subsample 

(edit) it would be nice in some way to combine |> and Fn.flip into a new operator to handle this case. But that’s a bit too sophisticated for me at this stage.


#3

It’s quite common to find that a value coming into “|>” is not the last argument of the function consuming it. I’m happy to write:

|> fun v -> Vec2d.sub v offset

#4

Hi @lindig,

Yes. That would work too.

Another option is to reorder when the functions are applied and use @@ to link the functions together, like:

let s = Vec2d.sub offset
     @@ Vec2d.create x y   
     |> Vec2d.scale d 
     |> Vec2d.add subsample 

But that seems less parsable to me as the creation of the data that is being operated over is moved into the middle of the set of operations on that data.

As I say what would be nice would be another operator such as |< that would apply |> Fn.flip to a function, making it read more like:

NOT REAL CODE

let s = Vec2d.create x y  
     |< Vec2d.sub offset
     |> Vec2d.scale d 
     |> Vec2d.add subsample

But maybe that is because my background is with Perl, so I love a good operator.


#5

Thinking about it and plying around, it is not too hard to write a function that does what I was thinking of:

let (|<) x f = fun y -> f y x

However the precedence doesn’t work, so it ends up looking like:

let s = (Vec2d.create x y  
     |< Vec2d.sub) offset
     |> Vec2d.scale d 
     |> Vec2d.add subsample

The only way I can think of that would ever allow it to work would be to have (and I apologise if my terminology is a bit off here) a precedence that is weekly coupled to the left - so that the first expression is evaluated and tightly coupled to the right so that the function can be wrapped. Failing that the only other way I could think of would be to be able to reverse a partially applied function so that the result of the left hand side could be injected in front of the partially applied value.


#6

I don’t find you |< example readable at all. It is very easy to mistake the operators as being the same¹, and misunderstand the semantics of your code.

What you learned at the beginning of this thread is that sometimes infix application operators can make code harder to understand than you thought. I don’t think that the solution to this problem is to introduce new operators. You should consider, however, using old operators. This looks more readable to me:

Vec2d.(scale d (create x y -! offset) +! subsample)

and there fixed associativity and precedence is a strength, not a weakness.