Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

missing each/map (¨) #69

Open
tcolgate opened this issue Dec 7, 2021 · 17 comments
Open

missing each/map (¨) #69

tcolgate opened this issue Dec 7, 2021 · 17 comments

Comments

@tcolgate
Copy link
Contributor

tcolgate commented Dec 7, 2021

It might be that I'm missing something, but mapping an operator over a vector is super useful and I had to jump through some hoops to do it.

Given a vector, compute the triangle number for each element (but any function would apply). I ended up defining the function as an operator that ignored one of its args. then reducing over a vector of pairs...

op a triangle b = (b * (b + 1)) / 2                                                                                    

op trianglevs vs = triangle/ (rho vs) 2 rho ((rho vs) rho 2) sel vs

Is a map / each syntactically difficult to support though, feels like it should be similar to reduce?

@tcolgate
Copy link
Contributor Author

tcolgate commented Dec 7, 2021

I fancy having a go at implementing this one myself, but was curious if there was an obvious reason it was left out.

@robpike
Copy link
Owner

robpike commented Dec 7, 2021

I've wanted it too. Let me tackle it, although it's a bit tricky. It might require a long-desired feature of having operators be first class, although special parser support might be enough.

In the meantime you can write a loop for a specific case, but you'd need a different loop for each operator.

@tcolgate
Copy link
Contributor Author

tcolgate commented Dec 8, 2021

Brilliant, thanks (I suspected it wasn't going to be as simple as adding another style of / or .).

In the meantime you can write a loop for a specific case, but you'd need a different loop for each operator.

By way of an experience report, I hit similar issues to as @rsc discussed in his recent PR when writing loops over matrices, where it was sometimes hard to work out the terminal conditions reliably.

@robpike
Copy link
Owner

robpike commented Dec 8, 2021

The core issue is that there is nothing in ivy corresponding to what are called operators in APL (ivy's operators are functions in APL speak). The "each" token represents an operator, which is something that modifies the behavior of a function.

I spent some time on this today and found a couple of ways to hack it in, but nothing that I'm happy with yet. As I said, I might go back to my original (and also not easy; I didn't plan for this) idea of allowing operators to be values themselves. Then an operator could also be an operand, which has many benefits (including allowing anonymous blocks of code to be operators).

@tcolgate
Copy link
Contributor Author

tcolgate commented Dec 8, 2021

Thanks for the elaboration. Reading around it seems that /, \ etc are also operators (and o. is a special cased one), all very interesting! Sounds like there a good argument for skipping later APL's approaches, and doing something more regular like J's approach (talking way beyond what I understand here).
Thanks for the time, thought, and information. I look forward to seeing what, if anything, comes from it all!

@rsc
Copy link
Contributor

rsc commented Dec 9, 2021

I've been thinking a lot about this. I agree that something's missing, and also that APL's each is not exactly right as is.

One thing I was considering is saying x op@ y to mean you run x op y[i] for all i in iota (rho y)[1] and then puts the results back along the first axis. You could imagine it cascading too, so that x op@@ y applies to all y[i][j] and reassembles the results into the first two axes. I think that would handle the majority of the tail-recursive loops I have written over the past week.

@rsc
Copy link
Contributor

rsc commented Dec 9, 2021

I forgot the second half of what I'd been thinking with @. It could be made to work on the other side, too, so that x @op y would mean to reassemble the results of x[i] op y. And then if it was on both sides, the indexes would be matched. So if you had a binary operator f written in such a way that its arguments must be scalars, then all of these would apply f elementwise same as a builtin like +.

1 f@ iota 5
(iota 5) f@ 1
(iota 5) @f@ (iota 5)
(3 3 rho iota 9) @@f@@ (3 3 rho iota 9)

or if you had something that needed vectors on both sides then

z = 3 3 rho iota 9
x = z @f@ z

would be given by x[i] = z[i] f z[i]

It's a little weird looking.

Another thing I've been wondering is whether there's any way to say what the maximum rank of an argument is, a kind of optional type in the operator definition. If you could define something like op x(1) f y(1) to mean that f can't handle more than a vector, then something like z f z could automatically figure out that it needs to iterate over axes until both sides are down to vectors, even more like +.

@robpike
Copy link
Owner

robpike commented Dec 9, 2021

I've been thinking a lot about this. I agree that something's missing, and also that APL's each is not exactly right as is.

One thing I was considering is saying x op@ y to mean you run x op y[i] for all i in iota (rho y)[1] and then puts the results back along the first axis. You could imagine it cascading too, so that x op@@ y applies to all y[i][j] and reassembles the results into the first two axes. I think that would handle the majority of the tail-recursive loops I have written over the past week.

This is pretty much what I've had in mind. The doubling action of the modifier is a nice wrinkle. I hadn't come up with a character yet, though. Not sure I like @ but that's what bikeshedding is for.

@robpike
Copy link
Owner

robpike commented Dec 11, 2021

I implemented, by analogy with o., map., to iterate over the elements of the right hand side. So

map.! 1 2 3

prints

1 2 6

It was easy to do but it's not useful. Almost all the unary operators already apply elementwise, including simple user-defined ones. The few that don't, such as rho and iota, produce results of varying sizes, and it's not easy to see how the results should pack given ivy's data model.

map.iota 1 2 3

would produce the integers 1 1 2 1 2 3 but not as a simple vector of scalars, but as 1 (1 2) (1 2 3), and ivy's data model does not allow compound elements in vectors, so this is an error.

So I think simple map is out. Dyalog's each (¨) works, but dyalog has a richer data model. The rhs of tryapl.org's example of its use is illegal in ivy.

Still thinking about rsc's idea.

@glxxyz
Copy link
Contributor

glxxyz commented Dec 15, 2021

I ended up defining the function as an operator that ignored one of its args. then reducing over a vector of pairs

In this example you could define a unary operator:

op triangle b = (b * (b + 1)) / 2

triangle 10
55

triangle iota 10
1 3 6 10 15 21 28 36 45 55

(some more complex functions require map/each as described in the rest of the thread)

@rsc
Copy link
Contributor

rsc commented Dec 16, 2021

I implemented @ in my copy of my Ivy, but I haven't sent it out. The implementation needs work and I don't know if it's the right idea anyway.

But it has one property that seems pretty nice. I was wrong about what xs @op@ ys would mean. I had said it would run op on corresponding pairs of x and y and collect the results, but that's inconsistent with the single-@ cases. If xs @op y means "for all x in xs, x op y" and "x op@ ys" means "for all y in ys, x op y", then the only consistent meaning of xs @op@ ys is "for all x in xs, for all y in ys, x op y". That is, it's an outer product, at least if xs and ys are vectors. More generally, x @...@op@...@ y is the outer product when the number of @'s on the left equals the rank of x and the number on the right equals the rank of y.

After using this for a few days (for example, here in this video) I find it much easier to think of the outer product this way. I never really got used to o.== but seeing @==@ as two loops works much better for me.

Rob's comment about map not being needed for the basic operators is absolutely true: they all automatically extend to vectors well already. It is far more useful for user-defined operators, which are often defined in ways that don't automatically extend to vectors (for example, later in the video).

One basic operator that does benefit is transp. If you have, say, a 5 16 16 called x, interpreted as 5 16x16 matrices and you want to transpose them all, you can (now) do 1 3 2 transp x, but transp@ x is a bit clearer.

@rsc
Copy link
Contributor

rsc commented Dec 27, 2021

I pushed my @ implementation in #83. It's not for review, just for discussion.
It does seem to work well at least for the Ivy programs I have been writing,
and I like that it is a simpler building block than outer product
(that is, outer product can be built naturally from it).

One possible alternative spelling would be "e." and ".e" instead of "@",
so that the example in the PR would be x e.dist.e y instead of x @dist@ y.
I'm not sure whether that's significantly better.
e is a reserved word and can't be the name of a user-defined operator,
so at least it's not ambiguous with inner product.

@robpike
Copy link
Owner

robpike commented Dec 28, 2021

I admit a purely subjective dislike for the notation '@', but do like what your functor can achieve. The letter 'e' seems an improvement to me, by analogy with 'o', but then 'o' didn't intentionally stand for "outer" but rather as an analog for the APL ∘ functor. As a result, to me, although 'e' is short, 'each' says more, and reads well:

x each.dist.each y

Although that is more verbose, it reads clearly. We'd probably get used to the single letter in time, though.

Analogy with dyalog is probably not worth pursuing, as I shied away (perhaps for simplicity, perhaps for timidity) of having elements of matrices and vectors be anything other than scalars.

So I'm leaning towards following your lead, but must also admit I'm not fully conversant with it yet.

@kpym
Copy link

kpym commented Dec 22, 2022

map.iota 1 2 3

would produce the integers 1 1 2 1 2 3 but not as a simple vector of scalars, but as 1 (1 2) (1 2 3), and ivy's data model does not allow compound elements in vectors, so this is an error.

What if we follow the <binary>.<binary> "strategy" for something like <binary>.<uintary>, where <unitary> will be applied to each element and <binary> will be used to reduce the result. For example ,.iota 1 2 3 will produce (iota 1), (iota 2), (iota 3) which is 1 1 2 1 2 3. Another example :

op ssq x = x>0: x**2; -x**2 # signed square
+.ssq 1 -2 3
6

because +.ssq 1 -2 3 will be the same as (ssq 1) + (ssq -2) + (ssq 3).

@fzipp
Copy link
Contributor

fzipp commented Aug 10, 2023

I pushed my @ implementation in #83. It's not for review, just for discussion. It does seem to work well at least for the Ivy programs I have been writing

@rsc, I have rebased your demo branch onto master to try it out it with the latest Ivy features.

I'm trying to make use of @ in the Game of Life in Ivy. In the original APL code, the ¨ operator is used to eliminate the repetitive rot operations in this part:

+/ +/% 1 0 -1 o.flip (1 rot R) (0 rot R) (-1 rot R)

I'm curious if it's feasible to use the @ operator in this context. 1 0 -1 @rot R unfortunately results in a matrix with dimensions 3 5 7 rather than the desired vector of length 3. I would need a way to split the 3d matrix into its constituent major cells, but I haven't found a way to do this in Ivy.

@fzipp
Copy link
Contributor

fzipp commented Aug 14, 2023

I'm curious if it's feasible to use the @ operator in this context.

Ok, I figured it out myself:

+/% +/% 1 0 -1 @flip@ 1 0 -1 @rot R

@superfrink
Copy link

I was looking for an "each" for a user defined operation and added the opmap keyword.

# mulunder20 returns the multiples of the input values that are under under 20.
op mulunder20 M = ((T mod M) == 0) sel T = -1 drop iota 20

opmap mulunder20 3
3 6 9 12 15 18

opmap mulunder20 3 5
(3 6 9 12 15 18) (5 10 15)

It does seem like it could be adding complexity and it might be better to make user defined operations first-class.

PR: superfrink#1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants