Basic element-wise operator set for Arrays, Arrays of Arrays, etc.


(Nicolas Fezans) #1

Dear all,

In swift (just as in many other languages) I have been terribly
missing the operators like .* ./ .^ as I know them from
MATLAB/Scilab. These operators are very handy and do element-wise
operations on vectors or matrices of the same size.

So for instance A*B is a matrix multiplication (and the number of
columns for A must correspond to the number of rows in B), whereas A*B
(with A and B of same size) returns the matrix of that size whose
elements are obtained by making the product of each pair of elements
at the same location in A and B.

So just a small example:
[1.0 , 2.5 , 3.0] .* [2.0 , 5.0 , -1.0] -> [2.0 , 12.5 , -3.0]

The same exists for the division (./) or for instance for the power
function (.^). Here another example with *, .* , ^ , and .^ to show
the difference in behaviour in MATLAB/Scilab

A = [1 2 3 ; 4 5 6 ; 7 8 9];
A*A

ans =

    30 36 42
    66 81 96
   102 126 150

A.*A

ans =

     1 4 9
    16 25 36
    49 64 81

A^2

ans =

    30 36 42
    66 81 96
   102 126 150

A.^3

ans =

     1 8 27
    64 125 216
   343 512 729

For addition and subtraction the regular operator (+ and -) and their
counterparts (.+ and .-) are actually doing the same. However note
that since the + operator on arrays is defined differently (it does an
append operation), there is a clear use for a .+ operation in swift.

Version 1:
In principle, we can define it recursively, for instance ...+ would be
the element-wise application of the ..+ operator, which is itself the
element-wise application of the .+ operator, which is also the
element-wise application of the + operator.

Version 2:
Alternatively we could have a concept where .+ is the element-wise
application of the .+ operator and finally when reaching the basic
type (e.g. Double when starting from [[[[Double]]]]) the .+ operator
needs to be defined as identical to the + operator. I do prefer this
version since it does not need to define various operators depending
on the "level" (i.e. Double -> level 0, [Double] -> level 1,
[[Double]] -> level 2, etc.). I could make this option work without
generics, but as I tried it with generics it generated a runtime error
as the call stack grew indefinitely (which does not seem as something
that should actually happen since at each call the level gets lower
and when reaching 0 it all solvable).

Anyway, I would like to discuss first the basic idea of defining these
element-wise operators for Arrays, before seeing how far it would be
interesting to go on this and how the implementation should exactly
look like. As a support for the discussion, you will find hereunder a
first shot for a generics-based solution for the aforementioned
Version 1 and going up to level 3 and for the 4 basic operators + - *
/
(BTW you can see that I have twice the same code, once with the
protocol conformance to my own protocols and once for FloatingPoint:
is there a way to specific the protocol conformance to protocol A or
to protocol B at once?)

I personally think that these operators are very practical and helping
programmers to directly "vectorize" the way they write their
operations. Often these element-wise operations replace loops and I
think that the required syntax analysis (on compiler's side) to
vectorize the code is much simpler then. In swift, I have been using
map, flatMap, zip + map with a closure to make these type of
operations, but I think that the proposed operators would be a much
clearer and expressive way of coding this for most basic operations.

Note that I mention and consider only Arrays here, but the idea might
be extended to other collections/containers.

I am very curious to see the feedback of the community on this!

Nicolas

infix operator .+
infix operator ..+
infix operator ...+
infix operator .-
infix operator ..-
infix operator ...-
infix operator .*
infix operator ..*
infix operator ...*
infix operator ./
infix operator ../
infix operator .../

protocol ImplementsInnerAddition { static func + (_: Self,_: Self)->Self }
protocol ImplementsInnerSubtraction { static func - (_: Self,_: Self)->Self }
protocol ImplementsInnerMultiplication { static func * (_: Self,_: Self)->Self }
protocol ImplementsInnerDivision { static func / (_: Self,_: Self)->Self }

func .+<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a + b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] + b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a + rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .+<T> (lhs: [T], rhs: T ) -> [T] where T:ImplementsInnerAddition
{ return lhs.map({(a: T)->T in return a + rhs }) }
func .+<T> (lhs: T , rhs: [T]) -> [T] where T:ImplementsInnerAddition
{ return rhs.map({(b: T)->T in return lhs + b }) }
func ..+<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .+ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..+<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerAddition { return lhs.map({(a: [T])->[T] in return
a .+ rhs }) }
func ..+<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerAddition { return rhs.map({(b: [T])->[T] in return
lhs .+ b }) }
func ...+<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..+ rhs[0] })
    }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...+<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerAddition { return lhs.map({(a: [[T]])->[[T]] in
return a ..+ rhs }) }
func ...+<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerAddition { return rhs.map({(b: [[T]])->[[T]] in
return lhs ..+ b }) }
func .-<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a - b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] - b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a - rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .-<T> (lhs: [T], rhs: T ) -> [T] where
T:ImplementsInnerSubtraction { return lhs.map({(a: T)->T in return a
- rhs }) }
func .-<T> (lhs: T , rhs: [T]) -> [T] where
T:ImplementsInnerSubtraction { return rhs.map({(b: T)->T in return
lhs - b }) }
func ..-<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..-<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerSubtraction { return lhs.map({(a: [T])->[T] in
return a .- rhs }) }
func ..-<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerSubtraction { return rhs.map({(b: [T])->[T] in
return lhs .- b }) }
func ...-<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...-<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerSubtraction { return lhs.map({(a: [[T]])->[[T]] in
return a ..- rhs }) }
func ...-<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerSubtraction { return rhs.map({(b: [[T]])->[[T]] in
return lhs ..- b }) }
func .*<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a * b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] * b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a * rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .*<T> (lhs: [T], rhs: T ) -> [T] where
T:ImplementsInnerMultiplication { return lhs.map({(a: T)->T in return
a * rhs }) }
func .*<T> (lhs: T , rhs: [T]) -> [T] where
T:ImplementsInnerMultiplication { return rhs.map({(b: T)->T in return
lhs * b }) }
func ..*<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..*<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerMultiplication { return lhs.map({(a: [T])->[T] in
return a .* rhs }) }
func ..*<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerMultiplication { return rhs.map({(b: [T])->[T] in
return lhs .* b }) }
func ...*<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...*<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerMultiplication { return lhs.map({(a: [[T]])->[[T]]
in return a ..* rhs }) }
func ...*<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerMultiplication { return rhs.map({(b: [[T]])->[[T]]
in return lhs ..* b }) }
func ./<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a / b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] / b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a / rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ./<T> (lhs: [T], rhs: T ) -> [T] where T:ImplementsInnerDivision
{ return lhs.map({(a: T)->T in return a / rhs }) }
func ./<T> (lhs: T , rhs: [T]) -> [T] where T:ImplementsInnerDivision
{ return rhs.map({(b: T)->T in return lhs / b }) }
func ../<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a ./ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] ./ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a ./ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ../<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerDivision { return lhs.map({(a: [T])->[T] in return
a ./ rhs }) }
func ../<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerDivision { return rhs.map({(b: [T])->[T] in return
lhs ./ b }) }
func .../<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ../ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ../ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ../ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .../<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerDivision { return lhs.map({(a: [[T]])->[[T]] in
return a ../ rhs }) }
func .../<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerDivision { return rhs.map({(b: [[T]])->[[T]] in
return lhs ../ b }) }
func .+<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a + b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] + b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a + rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .+<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a + rhs }) }
func .+<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs + b }) }
func ..+<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .+ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..+<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .+ rhs }) }
func ..+<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .+ b }) }
func ...+<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..+ rhs[0] })
    }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...+<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..+
rhs }) }
func ...+<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..+
b }) }
func .-<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a - b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] - b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a - rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .-<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a - rhs }) }
func .-<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs - b }) }
func ..-<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..-<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .- rhs }) }
func ..-<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .- b }) }
func ...-<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...-<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..-
rhs }) }
func ...-<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..-
b }) }
func .*<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a * b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] * b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a * rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .*<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a * rhs }) }
func .*<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs * b }) }
func ..*<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..*<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .* rhs }) }
func ..*<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .* b }) }
func ...*<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...*<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..*
rhs }) }
func ...*<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..*
b }) }
func ./<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a / b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] / b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a / rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ./<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a / rhs }) }
func ./<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs / b }) }
func ../<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a ./ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] ./ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a ./ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ../<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a ./ rhs }) }
func ../<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs ./ b }) }
func .../<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ../ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ../ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ../ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .../<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ../
rhs }) }
func .../<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ../
b }) }


(David Sweeris) #2

In Xcode 8.2.1, with the 8.2.1 toolchain, this works (well, it compiles… obviously it doesn’t check for mis-matched array lengths):
infix operator .+
func .+ <T: Integer> (lhs: [T], rhs: [T]) -> [T] {
    return zip(lhs, rhs).map { $0.0 + $0.1 }
}
print([1,2,3].+[4,5,6]) //outputs [5, 7, 9]

- Dave Sweeris

···

On Feb 17, 2017, at 10:38 AM, Abe Schneider via swift-evolution <swift-evolution@swift.org> wrote:

If I read Nicolas's post correctly, I think he's more arguing for the
ability to create syntax that allows Swift to behave in a similar way
to Numpy/Matlab. While Swift already does allow you to define your own
operators, the main complaint is that he can't define the specific
operators he would like.


(David Sweeris) #3

If I read Nicolas's post correctly, I think he's more arguing for the
ability to create syntax that allows Swift to behave in a similar way
to Numpy/Matlab. While Swift already does allow you to define your own
operators, the main complaint is that he can't define the specific
operators he would like.

In Xcode 8.2.1, with the 8.2.1 toolchain, this works (well, it compiles… obviously it doesn’t check for mis-matched array lengths):
infix operator .+
func .+ <T: Integer> (lhs: [T], rhs: [T]) -> [T] {

precondition(lhs.count == rhs.count)

    return zip(lhs, rhs).map { $0.0 + $0.1 }
}
print([1,2,3].+[4,5,6]) //outputs [5, 7, 9]

There's nothing, afaik, which stands in the way of that syntax today.

Yep, that’s what I was demonstrating

The proposal is to extend the standard library to add syntax for a math library. The idea of having a core math library has already been mentioned on this list, to great approval, but it should come in the form of an actual library, and not a syntax only!

Oh, sorry, I was confused… I thought the proposal was to allow the syntax.

Well, count me as another +1 for adding a `CoreMath` library (although it should probably be called something else, unless we can make it work in Obj-C, too).

- Dave Sweeris

···

On Feb 17, 2017, at 10:51 AM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Fri, Feb 17, 2017 at 12:46 PM, David Sweeris <davesweeris@mac.com <mailto:davesweeris@mac.com>> wrote:

On Feb 17, 2017, at 10:38 AM, Abe Schneider via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Stephen Canon) #4

Right. IMO the way to do this is to develop such a library outside of the stdlib to the point that it’s useful enough to be employed for real work and people can evaluate what works and what doesn’t. That may require small language and stdlib changes for support, which should be brought-up here.

Once such a library were reasonably mature, it would be reasonable to propose it for inclusion in swift proper. I expect this process will take a couple *years*.

FWIW, I personally despise MATLAB .operator notation, though I accept that it’s pretty much achieved saturation at this point. It looks reasonably nice for simple single-operand expressions, but it imposes a large burden on the compiler (and often leads to inefficient code). In this regard, they are something of an attractive nuisance. Consider your example:

  3.0 .* A .* B ./ (C.^ 4.0)

with the most obvious implementation, this generates four calls to vector functions:

  - multiply array by scalar (tmp0 <— 3 .* A)
  - elementwise multiplication (tmp1 <— tmp0 .* B)
  - elementwise exponentiation (tmp2 <— C .^ 4)
  - elementwise division (result <— tmp1 ./ tmp2)

again, with the obvious implementation, this wastes space for temporaries and results in extraneous passes through the data. It is often *possible* to solve these issues (at least for some the most common cases) by producing proxy objects that can fuse loops, but that gets very messy very fast, and it’s ton of work to support all the interesting cases.

On the other hand, the stupid obvious loop:

  for i in 0 ..< count {
    result[i] = 3*A[i]*B[i]/(C[i]*C[i]*C[i]*C[i])
  }

or the cleaner, with a little sugar:
  
  zip3(A,B,C).map { 3 * $0.0 * $0.1 / ($0.2 ^ 4) }

requires a tiny bit of boilerplate, but only a single pass through the data and allows the compiler to vectorize. Even if the four vector functions use by the .operations are perfectly hand-optimized, the multiple passes and extra memory traffic they entail often makes it *slower* than the stupid for loop.

I don’t mean to be too discouraging; all of these issues are surmountable, but I (personally) think there’s a lot of development that should happen *outside* of the stdlib before such a feature is considered for inclusion in the stdlib.

– Steve

···

On Feb 17, 2017, at 10:51 AM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

There's nothing, afaik, which stands in the way of that syntax today. The proposal is to extend the standard library to add syntax for a math library. The idea of having a core math library has already been mentioned on this list, to great approval, but it should come in the form of an actual library, and not a syntax only!


(Xiaodi Wu) #5

If I read Nicolas's post correctly, I think he's more arguing for the
ability to create syntax that allows Swift to behave in a similar way
to Numpy/Matlab. While Swift already does allow you to define your own
operators, the main complaint is that he can't define the specific
operators he would like.

In Xcode 8.2.1, with the 8.2.1 toolchain, this works (well, it compiles…
obviously it doesn’t check for mis-matched array lengths):
infix operator .+
func .+ <T: Integer> (lhs: [T], rhs: [T]) -> [T] {

precondition(lhs.count == rhs.count)

    return zip(lhs, rhs).map { $0.0 + $0.1 }
}
print([1,2,3].+[4,5,6]) //outputs [5, 7, 9]

There's nothing, afaik, which stands in the way of that syntax today. The
proposal is to extend the standard library to add syntax for a math
library. The idea of having a core math library has already been mentioned
on this list, to great approval, but it should come in the form of an
actual library, and not a syntax only!

···

On Fri, Feb 17, 2017 at 12:46 PM, David Sweeris <davesweeris@mac.com> wrote:

On Feb 17, 2017, at 10:38 AM, Abe Schneider via swift-evolution < > swift-evolution@swift.org> wrote:


(Abe Schneider) #6

If I read Nicolas's post correctly, I think he's more arguing for the
ability to create syntax that allows Swift to behave in a similar way
to Numpy/Matlab. While Swift already does allow you to define your own
operators, the main complaint is that he can't define the specific
operators he would like.

I've been working on a Tensor library that would also benefit from
this. I ended up creating unicode operators for inner product etc. and
then used the standard operators for elementwise operations. However,
I think there is some virtue in not having to use the unicode
characters (many people don't want to have to remap their keyboard),
so providing alternatives might be nice.

While I've never been a fan of Matlab's notation, other people might
be familiar with it, so there's some virtue in making it available.

···

On Fri, Feb 17, 2017 at 1:01 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

If you're simply looking for elementwise multiply without performance
requirements, map(*) is a very succinct spelling.

Performant implementations for these operations like you have in Matlab rely
on special math libraries. Apple platforms have Accelerate that makes this
possible, and other implementations of BLAS/LAPACK do the same for Linux and
Windows platforms.

There has been talk on this list of writing Swifty wrappers for such
libraries. The core team has said that the way to get such facilities into
Swift corelibs is to write your own library, get broad adoption, then
propose its acceptance here. Currently, several libraries like Surge and
Upsurge offer vectorized wrappers in Swifty syntax for Apple platforms; it
would be interesting to explore whether the same can be done in a
cross-platform way.

But simply adding sugar to the standard library will not give you the
results you're looking for (by which I mean, the performance will be
unacceptable), and there's no point in providing sugar for something that
doesn't work like the operator implies (Matlab's elementwise operators offer
_great_ performance).

On Fri, Feb 17, 2017 at 11:46 Nicolas Fezans via swift-evolution > <swift-evolution@swift.org> wrote:

Dear all,

In swift (just as in many other languages) I have been terribly
missing the operators like .* ./ .^ as I know them from
MATLAB/Scilab. These operators are very handy and do element-wise
operations on vectors or matrices of the same size.

So for instance A*B is a matrix multiplication (and the number of
columns for A must correspond to the number of rows in B), whereas A*B
(with A and B of same size) returns the matrix of that size whose
elements are obtained by making the product of each pair of elements
at the same location in A and B.

So just a small example:
[1.0 , 2.5 , 3.0] .* [2.0 , 5.0 , -1.0] -> [2.0 , 12.5 , -3.0]

The same exists for the division (./) or for instance for the power
function (.^). Here another example with *, .* , ^ , and .^ to show
the difference in behaviour in MATLAB/Scilab

>> A = [1 2 3 ; 4 5 6 ; 7 8 9];
>> A*A

ans =

    30 36 42
    66 81 96
   102 126 150

>> A.*A

ans =

     1 4 9
    16 25 36
    49 64 81

>> A^2

ans =

    30 36 42
    66 81 96
   102 126 150

>> A.^3

ans =

     1 8 27
    64 125 216
   343 512 729

For addition and subtraction the regular operator (+ and -) and their
counterparts (.+ and .-) are actually doing the same. However note
that since the + operator on arrays is defined differently (it does an
append operation), there is a clear use for a .+ operation in swift.

Version 1:
In principle, we can define it recursively, for instance ...+ would be
the element-wise application of the ..+ operator, which is itself the
element-wise application of the .+ operator, which is also the
element-wise application of the + operator.

Version 2:
Alternatively we could have a concept where .+ is the element-wise
application of the .+ operator and finally when reaching the basic
type (e.g. Double when starting from [[[[Double]]]]) the .+ operator
needs to be defined as identical to the + operator. I do prefer this
version since it does not need to define various operators depending
on the "level" (i.e. Double -> level 0, [Double] -> level 1,
[[Double]] -> level 2, etc.). I could make this option work without
generics, but as I tried it with generics it generated a runtime error
as the call stack grew indefinitely (which does not seem as something
that should actually happen since at each call the level gets lower
and when reaching 0 it all solvable).

Anyway, I would like to discuss first the basic idea of defining these
element-wise operators for Arrays, before seeing how far it would be
interesting to go on this and how the implementation should exactly
look like. As a support for the discussion, you will find hereunder a
first shot for a generics-based solution for the aforementioned
Version 1 and going up to level 3 and for the 4 basic operators + - *
/
(BTW you can see that I have twice the same code, once with the
protocol conformance to my own protocols and once for FloatingPoint:
is there a way to specific the protocol conformance to protocol A or
to protocol B at once?)

I personally think that these operators are very practical and helping
programmers to directly "vectorize" the way they write their
operations. Often these element-wise operations replace loops and I
think that the required syntax analysis (on compiler's side) to
vectorize the code is much simpler then. In swift, I have been using
map, flatMap, zip + map with a closure to make these type of
operations, but I think that the proposed operators would be a much
clearer and expressive way of coding this for most basic operations.

Note that I mention and consider only Arrays here, but the idea might
be extended to other collections/containers.

I am very curious to see the feedback of the community on this!

Nicolas

infix operator .+
infix operator ..+
infix operator ...+
infix operator .-
infix operator ..-
infix operator ...-
infix operator .*
infix operator ..*
infix operator ...*
infix operator ./
infix operator ../
infix operator .../

protocol ImplementsInnerAddition { static func + (_: Self,_:
Self)->Self }
protocol ImplementsInnerSubtraction { static func - (_: Self,_:
Self)->Self }
protocol ImplementsInnerMultiplication { static func * (_: Self,_:
Self)->Self }
protocol ImplementsInnerDivision { static func / (_: Self,_:
Self)->Self }

func .+<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a + b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] + b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a + rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .+<T> (lhs: [T], rhs: T ) -> [T] where T:ImplementsInnerAddition
{ return lhs.map({(a: T)->T in return a + rhs }) }
func .+<T> (lhs: T , rhs: [T]) -> [T] where T:ImplementsInnerAddition
{ return rhs.map({(b: T)->T in return lhs + b }) }
func ..+<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .+ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..+<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerAddition { return lhs.map({(a: [T])->[T] in return
a .+ rhs }) }
func ..+<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerAddition { return rhs.map({(b: [T])->[T] in return
lhs .+ b }) }
func ...+<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..+ rhs[0] })
    }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...+<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerAddition { return lhs.map({(a: [[T]])->[[T]] in
return a ..+ rhs }) }
func ...+<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerAddition { return rhs.map({(b: [[T]])->[[T]] in
return lhs ..+ b }) }
func .-<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerSubtraction
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a - b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] - b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a - rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .-<T> (lhs: [T], rhs: T ) -> [T] where
T:ImplementsInnerSubtraction { return lhs.map({(a: T)->T in return a
- rhs }) }
func .-<T> (lhs: T , rhs: [T]) -> [T] where
T:ImplementsInnerSubtraction { return rhs.map({(b: T)->T in return
lhs - b }) }
func ..-<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..-<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerSubtraction { return lhs.map({(a: [T])->[T] in
return a .- rhs }) }
func ..-<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerSubtraction { return rhs.map({(b: [T])->[T] in
return lhs .- b }) }
func ...-<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...-<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerSubtraction { return lhs.map({(a: [[T]])->[[T]] in
return a ..- rhs }) }
func ...-<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerSubtraction { return rhs.map({(b: [[T]])->[[T]] in
return lhs ..- b }) }
func .*<T> (lhs: [T], rhs: [T]) -> [T] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a * b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] * b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a * rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .*<T> (lhs: [T], rhs: T ) -> [T] where
T:ImplementsInnerMultiplication { return lhs.map({(a: T)->T in return
a * rhs }) }
func .*<T> (lhs: T , rhs: [T]) -> [T] where
T:ImplementsInnerMultiplication { return rhs.map({(b: T)->T in return
lhs * b }) }
func ..*<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..*<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerMultiplication { return lhs.map({(a: [T])->[T] in
return a .* rhs }) }
func ..*<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerMultiplication { return rhs.map({(b: [T])->[T] in
return lhs .* b }) }
func ...*<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...*<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerMultiplication { return lhs.map({(a: [[T]])->[[T]]
in return a ..* rhs }) }
func ...*<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerMultiplication { return rhs.map({(b: [[T]])->[[T]]
in return lhs ..* b }) }
func ./<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a / b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] / b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a / rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ./<T> (lhs: [T], rhs: T ) -> [T] where T:ImplementsInnerDivision
{ return lhs.map({(a: T)->T in return a / rhs }) }
func ./<T> (lhs: T , rhs: [T]) -> [T] where T:ImplementsInnerDivision
{ return rhs.map({(b: T)->T in return lhs / b }) }
func ../<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a ./ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] ./ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a ./ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ../<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerDivision { return lhs.map({(a: [T])->[T] in return
a ./ rhs }) }
func ../<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerDivision { return rhs.map({(b: [T])->[T] in return
lhs ./ b }) }
func .../<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ../ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ../ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ../ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .../<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerDivision { return lhs.map({(a: [[T]])->[[T]] in
return a ../ rhs }) }
func .../<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerDivision { return rhs.map({(b: [[T]])->[[T]] in
return lhs ../ b }) }
func .+<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a + b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] + b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a + rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .+<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a + rhs }) }
func .+<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs + b }) }
func ..+<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .+ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..+<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .+ rhs }) }
func ..+<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .+ b }) }
func ...+<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..+ rhs[0] })
    }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...+<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..+
rhs }) }
func ...+<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..+
b }) }
func .-<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a - b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] - b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a - rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .-<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a - rhs }) }
func .-<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs - b }) }
func ..-<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..-<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .- rhs }) }
func ..-<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .- b }) }
func ...-<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...-<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..-
rhs }) }
func ...-<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..-
b }) }
func .*<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a * b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] * b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a * rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .*<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a * rhs }) }
func .*<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs * b }) }
func ..*<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..*<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .* rhs }) }
func ..*<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .* b }) }
func ...*<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...*<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..*
rhs }) }
func ...*<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..*
b }) }
func ./<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a / b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] / b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a / rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ./<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a / rhs }) }
func ./<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs / b }) }
func ../<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a ./ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] ./ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a ./ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ../<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a ./ rhs }) }
func ../<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs ./ b }) }
func .../<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ../ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ../ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ../ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .../<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ../
rhs }) }
func .../<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ../
b }) }
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #7

If you're simply looking for elementwise multiply without performance
requirements, map(*) is a very succinct spelling.

Performant implementations for these operations like you have in Matlab
rely on special math libraries. Apple platforms have Accelerate that makes
this possible, and other implementations of BLAS/LAPACK do the same for
Linux and Windows platforms.

There has been talk on this list of writing Swifty wrappers for such
libraries. The core team has said that the way to get such facilities into
Swift corelibs is to write your own library, get broad adoption, then
propose its acceptance here. Currently, several libraries like Surge and
Upsurge offer vectorized wrappers in Swifty syntax for Apple platforms; it
would be interesting to explore whether the same can be done in a
cross-platform way.

But simply adding sugar to the standard library will not give you the
results you're looking for (by which I mean, the performance will be
unacceptable), and there's no point in providing sugar for something that
doesn't work like the operator implies (Matlab's elementwise operators
offer _great_ performance).

···

On Fri, Feb 17, 2017 at 11:46 Nicolas Fezans via swift-evolution < swift-evolution@swift.org> wrote:

Dear all,

In swift (just as in many other languages) I have been terribly
missing the operators like .* ./ .^ as I know them from
MATLAB/Scilab. These operators are very handy and do element-wise
operations on vectors or matrices of the same size.

So for instance A*B is a matrix multiplication (and the number of
columns for A must correspond to the number of rows in B), whereas A*B
(with A and B of same size) returns the matrix of that size whose
elements are obtained by making the product of each pair of elements
at the same location in A and B.

So just a small example:
[1.0 , 2.5 , 3.0] .* [2.0 , 5.0 , -1.0] -> [2.0 , 12.5 , -3.0]

The same exists for the division (./) or for instance for the power
function (.^). Here another example with *, .* , ^ , and .^ to show
the difference in behaviour in MATLAB/Scilab

>> A = [1 2 3 ; 4 5 6 ; 7 8 9];
>> A*A

ans =

    30 36 42
    66 81 96
   102 126 150

>> A.*A

ans =

     1 4 9
    16 25 36
    49 64 81

>> A^2

ans =

    30 36 42
    66 81 96
   102 126 150

>> A.^3

ans =

     1 8 27
    64 125 216
   343 512 729

For addition and subtraction the regular operator (+ and -) and their
counterparts (.+ and .-) are actually doing the same. However note
that since the + operator on arrays is defined differently (it does an
append operation), there is a clear use for a .+ operation in swift.

Version 1:
In principle, we can define it recursively, for instance ...+ would be
the element-wise application of the ..+ operator, which is itself the
element-wise application of the .+ operator, which is also the
element-wise application of the + operator.

Version 2:
Alternatively we could have a concept where .+ is the element-wise
application of the .+ operator and finally when reaching the basic
type (e.g. Double when starting from [[[[Double]]]]) the .+ operator
needs to be defined as identical to the + operator. I do prefer this
version since it does not need to define various operators depending
on the "level" (i.e. Double -> level 0, [Double] -> level 1,
[[Double]] -> level 2, etc.). I could make this option work without
generics, but as I tried it with generics it generated a runtime error
as the call stack grew indefinitely (which does not seem as something
that should actually happen since at each call the level gets lower
and when reaching 0 it all solvable).

Anyway, I would like to discuss first the basic idea of defining these
element-wise operators for Arrays, before seeing how far it would be
interesting to go on this and how the implementation should exactly
look like. As a support for the discussion, you will find hereunder a
first shot for a generics-based solution for the aforementioned
Version 1 and going up to level 3 and for the 4 basic operators + - *
/
(BTW you can see that I have twice the same code, once with the
protocol conformance to my own protocols and once for FloatingPoint:
is there a way to specific the protocol conformance to protocol A or
to protocol B at once?)

I personally think that these operators are very practical and helping
programmers to directly "vectorize" the way they write their
operations. Often these element-wise operations replace loops and I
think that the required syntax analysis (on compiler's side) to
vectorize the code is much simpler then. In swift, I have been using
map, flatMap, zip + map with a closure to make these type of
operations, but I think that the proposed operators would be a much
clearer and expressive way of coding this for most basic operations.

Note that I mention and consider only Arrays here, but the idea might
be extended to other collections/containers.

I am very curious to see the feedback of the community on this!

Nicolas

infix operator .+
infix operator ..+
infix operator ...+
infix operator .-
infix operator ..-
infix operator ...-
infix operator .*
infix operator ..*
infix operator ...*
infix operator ./
infix operator ../
infix operator .../

protocol ImplementsInnerAddition { static func + (_: Self,_:
Self)->Self }
protocol ImplementsInnerSubtraction { static func - (_: Self,_:
Self)->Self }
protocol ImplementsInnerMultiplication { static func * (_: Self,_:
Self)->Self }
protocol ImplementsInnerDivision { static func / (_: Self,_:
Self)->Self }

func .+<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a + b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] + b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a + rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .+<T> (lhs: [T], rhs: T ) -> [T] where T:ImplementsInnerAddition
{ return lhs.map({(a: T)->T in return a + rhs }) }
func .+<T> (lhs: T , rhs: [T]) -> [T] where T:ImplementsInnerAddition
{ return rhs.map({(b: T)->T in return lhs + b }) }
func ..+<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .+ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..+<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerAddition { return lhs.map({(a: [T])->[T] in return
a .+ rhs }) }
func ..+<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerAddition { return rhs.map({(b: [T])->[T] in return
lhs .+ b }) }
func ...+<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerAddition {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..+ rhs[0] })
    }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...+<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerAddition { return lhs.map({(a: [[T]])->[[T]] in
return a ..+ rhs }) }
func ...+<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerAddition { return rhs.map({(b: [[T]])->[[T]] in
return lhs ..+ b }) }
func .-<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a - b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] - b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a - rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .-<T> (lhs: [T], rhs: T ) -> [T] where
T:ImplementsInnerSubtraction { return lhs.map({(a: T)->T in return a
- rhs }) }
func .-<T> (lhs: T , rhs: [T]) -> [T] where
T:ImplementsInnerSubtraction { return rhs.map({(b: T)->T in return
lhs - b }) }
func ..-<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..-<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerSubtraction { return lhs.map({(a: [T])->[T] in
return a .- rhs }) }
func ..-<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerSubtraction { return rhs.map({(b: [T])->[T] in
return lhs .- b }) }
func ...-<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerSubtraction {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...-<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerSubtraction { return lhs.map({(a: [[T]])->[[T]] in
return a ..- rhs }) }
func ...-<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerSubtraction { return rhs.map({(b: [[T]])->[[T]] in
return lhs ..- b }) }
func .*<T> (lhs: [T], rhs: [T]) -> [T] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a * b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] * b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a * rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .*<T> (lhs: [T], rhs: T ) -> [T] where
T:ImplementsInnerMultiplication { return lhs.map({(a: T)->T in return
a * rhs }) }
func .*<T> (lhs: T , rhs: [T]) -> [T] where
T:ImplementsInnerMultiplication { return rhs.map({(b: T)->T in return
lhs * b }) }
func ..*<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..*<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerMultiplication { return lhs.map({(a: [T])->[T] in
return a .* rhs }) }
func ..*<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerMultiplication { return rhs.map({(b: [T])->[T] in
return lhs .* b }) }
func ...*<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerMultiplication {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...*<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerMultiplication { return lhs.map({(a: [[T]])->[[T]]
in return a ..* rhs }) }
func ...*<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerMultiplication { return rhs.map({(b: [[T]])->[[T]]
in return lhs ..* b }) }
func ./<T> (lhs: [T], rhs: [T]) -> [T] where T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a / b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] / b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a / rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ./<T> (lhs: [T], rhs: T ) -> [T] where T:ImplementsInnerDivision
{ return lhs.map({(a: T)->T in return a / rhs }) }
func ./<T> (lhs: T , rhs: [T]) -> [T] where T:ImplementsInnerDivision
{ return rhs.map({(b: T)->T in return lhs / b }) }
func ../<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where
T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a ./ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] ./ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a ./ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ../<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where
T:ImplementsInnerDivision { return lhs.map({(a: [T])->[T] in return
a ./ rhs }) }
func ../<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where
T:ImplementsInnerDivision { return rhs.map({(b: [T])->[T] in return
lhs ./ b }) }
func .../<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerDivision {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ../ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ../ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ../ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .../<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:ImplementsInnerDivision { return lhs.map({(a: [[T]])->[[T]] in
return a ../ rhs }) }
func .../<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:ImplementsInnerDivision { return rhs.map({(b: [[T]])->[[T]] in
return lhs ../ b }) }
func .+<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a + b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] + b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a + rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .+<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a + rhs }) }
func .+<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs + b }) }
func ..+<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .+ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..+<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .+ rhs }) }
func ..+<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .+ b }) }
func ...+<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..+ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..+ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..+ rhs[0] })
    }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...+<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..+
rhs }) }
func ...+<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..+
b }) }
func .-<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a - b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] - b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a - rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .-<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a - rhs }) }
func .-<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs - b }) }
func ..-<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..-<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .- rhs }) }
func ..-<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .- b }) }
func ...-<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..- b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..- b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..- rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...-<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..-
rhs }) }
func ...-<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..-
b }) }
func .*<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a * b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] * b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a * rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .*<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a * rhs }) }
func .*<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs * b }) }
func ..*<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a .* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] .* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a .* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ..*<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a .* rhs }) }
func ..*<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs .* b }) }
func ...*<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ..* b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ..* b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ..* rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ...*<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ..*
rhs }) }
func ...*<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ..*
b }) }
func ./<T> (lhs: [T], rhs: [T]) -> [T] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
T,b: T)->T in return a / b }) }
    guard (lhs.count != 1) else { return rhs.map({(b: T)->T
           in return lhs[0] / b }) }
    guard (rhs.count != 1) else { return lhs.map({(a: T)->T
           in return a / rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ./<T> (lhs: [T], rhs: T ) -> [T] where T:FloatingPoint { return
lhs.map({(a: T)->T in return a / rhs }) }
func ./<T> (lhs: T , rhs: [T]) -> [T] where T:FloatingPoint { return
rhs.map({(b: T)->T in return lhs / b }) }
func ../<T> (lhs: [[T]], rhs: [[T]]) -> [[T]] where T:FloatingPoint {
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[T],b: [T])->[T] in return a ./ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[T])->[T] in return lhs[0] ./ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[T])->[T] in return a ./ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func ../<T> (lhs: [[T]], rhs: [T] ) -> [[T]] where T:FloatingPoint {
return lhs.map({(a: [T])->[T] in return a ./ rhs }) }
func ../<T> (lhs: [T] , rhs: [[T]]) -> [[T]] where T:FloatingPoint {
return rhs.map({(b: [T])->[T] in return lhs ./ b }) }
func .../<T> (lhs: [[[T]]], rhs: [[[T]]]) -> [[[T]]] where T:FloatingPoint
{
    guard (lhs.count != rhs.count) else { return zip(lhs,rhs).map({(a:
[[T]],b: [[T]])->[[T]] in return a ../ b }) }
    guard (lhs.count != 1) else { return rhs.map({(b:
[[T]])->[[T]] in return lhs[0] ../ b }) }
    guard (rhs.count != 1) else { return lhs.map({(a:
[[T]])->[[T]] in return a ../ rhs[0] }) }
    assert(false,"Element-wise operation can only be applied to arrays
of same size or alternatively if one of the array is of size
1",file:#file,line:#line)
}
func .../<T> (lhs: [[[T]]], rhs: [[T]] ) -> [[[T]]] where
T:FloatingPoint { return lhs.map({(a: [[T]])->[[T]] in return a ../
rhs }) }
func .../<T> (lhs: [[T]] , rhs: [[[T]]]) -> [[[T]]] where
T:FloatingPoint { return rhs.map({(b: [[T]])->[[T]] in return lhs ../
b }) }
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Nicolas Fezans) #8

If you're simply looking for elementwise multiply without performance
requirements, map(*) is a very succinct spelling.

Yes, it is (combined with zip), but:
1) zip map will not enforce same size (which shall be done to fail hard
early), nor allow to combine with an array of a single element, nor with a
single element (same type but not even wrapped in the array)
2) the syntax becomes a bit uglier if you want an element-wise
multiplication of two [[[Double]]]
3) And the syntax I propose allow scientist to write things like (here
Version 2)

    3.0 .* A .* B ./ (C.^ 4.0)

which is extremely clear to read and look pretty much as we would write in
maths. BTW some of the functionalities I am mentioning here are existing
with no specific libraries with the std::valarray in C++.

Performant implementations for these operations like you have in Matlab

rely

on special math libraries. Apple platforms have Accelerate that makes this
possible, and other implementations of BLAS/LAPACK do the same for Linux

and

Windows platforms.

There has been talk on this list of writing Swifty wrappers for such
libraries. The core team has said that the way to get such facilities into
Swift corelibs is to write your own library, get broad adoption, then
propose its acceptance here. Currently, several libraries like Surge and
Upsurge offer vectorized wrappers in Swifty syntax for Apple platforms; it
would be interesting to explore whether the same can be done in a
cross-platform way.

But simply adding sugar to the standard library will not give you the
results you're looking for (by which I mean, the performance will be
unacceptable), and there's no point in providing sugar for something that
doesn't work like the operator implies (Matlab's elementwise operators

offer

_great_ performance).

Yes this is clear to me. My proposal is rather on bringing these syntax to
the language and the code given is only here to illustrate the kind of
behavior that I desire. Whether the computations are delegate to external
libraries, etc. is something that is very important for the performance but
that *should happen behind the scene* and which is IMO relatively decoupled
from the syntax exposed to the programmers.

I would like to insist on a particular point which is of daily concern to
me at work, where I cannot really use swift (at least not yet) and do these
kinds of things in C or C++. One issues that we have in academia is that we
often have algorithms developed independently by different groups of people
in different projects and for many years with no intentions to bring them
together.

Team A decides to built a solution based on a dedicated library libA
whereas team B decides to use another library libB for providing these
functionalities. Often it might even be the same library but in completely
different versions because there was 10 years between both developments.
Now a project C started and need to reuse pieces from project A and B but
the libraries do not "talk to each other": a matrix-type from libA is
different from a matrix-type from libB and you must refactor most of the
code to get the whole thing work together.

This situation also never happen (or to a very limited extend) in Matlab,
because the whole basic objects, matrices and so on are directly part of
the language and not provided by external libraries. The call to the highly
performant BLAS/LAPACK libraries happen behind the scenes and in
99.999999999% of the cases you never have to take care of anything. If
swift was integrating the right syntaxes in the language, my guess is that
many scientists who currently use C++ would be very interested (just as
most of them were preferring FORTRAN to C/C++ a long time ago).

Nicolas


(Nicolas Fezans) #9

Once such a library were reasonably mature, it would be reasonable to

propose it for inclusion in swift proper. I expect this process will take a
couple *years*.

Yes, I do not expect this to come very quickly either but if no one gets
started, that is going to last for even longer :wink:

or the cleaner, with a little sugar:

zip3(A,B,C).map { 3 * $0.0 * $0.1 / ($0.2 ^ 4) }

OK I guess I should have put a few regular matrix multiplications in the
middle to prevent from having such relatively straightforward solutions
(the thread was originally of element-wise but we now are clearly talking
about way more than that). Anyway, it is not worth opening a debate on the
example.

Well, count me as another +1 for adding a `CoreMath` library (although it

should probably be called something else, unless we can make it work in
Obj-C, too).

Well I was rather thinking of making a Swift-only library (at least at
first) but that would also be available for other platforms e.g Linux or
maybe some day on Windows => also working with reduced performance without
the Accelerator Framework but leveraging it on Apple Platforms (and
possibly leveraging others on the other platforms). This said I am open to
discussion on this... but having a very nice syntax for swift and having an
close to one-to-one equivalent also for Objective-C will probably add quite
some difficulties.

Consider your example:

3.0 .* A .* B ./ (C.^ 4.0)

with the most obvious implementation, this generates four calls to vector

functions:

- multiply array by scalar (tmp0 <— 3 .* A)
- elementwise multiplication (tmp1 <— tmp0 .* B)
- elementwise exponentiation (tmp2 <— C .^ 4)
- elementwise division (result <— tmp1 ./ tmp2)

again, with the obvious implementation, this wastes space for temporaries

and results in extraneous passes through the data. It is often *possible*
to solve these issues (at least for some the most common cases) by
producing proxy objects that can fuse loops, but that gets very messy very
fast, and it’s ton of work to support all the interesting cases.

This is clear to me and to be honest with you I am not really sure of the
best strategy to make this.

I don't think that the primary target for the library should be to deliver
the highest performance possible.
  => People who do need that level of performance would still need to
analyze and optimize their code by themselves and/or directly call the
Acceleration Framework or other specialized libraries.

What I would like to reach instead is rather what I would call "the highest
usability possible with decent performance". Some programmers will be
satisfied with the level of performance and will enjoy the readability and
maintainability of the code based of the library, whereas other will go for
more performant libraries (and that is perfectly fine!). Actually, I would
even expect later that some of those who belong to the latter category will
start experimenting with the easy but less performant library (lets call it
here "easy maths library") and optimize their code based on a high
performance library only in a second step.

My idea of a possibly pragmatic roadmap (which can be followed in that
order) to make such a library almost from scratch with the long-term goal
of being quite performant but primarily very easy to use could be:

1) think about the integration to the language, the syntax, the high-level
user documentation, etc. and demonstrate all this based on a relatively low
performance implementation

2) generate a collection a typical operations where the low-level libraries
offer very nice performance or where a clever handling of the temporary
variables is possible

3) implement these cases as specialized functions

4) "somehow" (I am clearly not a compiler guy) teach the compiler how to
analyze a generic expression that is based the defined object to find (if
possible) some identical but more performant substitutes for the "dumb"
successive calls of each operator function causing the generation of all
these possibly unnecessary temporaries.

This roadmap and especially having point #1 at the beginning does not
really follow the citation made earlier in the discussion by Xiaodi Wu on
the _method_ proposed by the core team:

"... And members of the core team have supported that idea. However, they

have stated that the _method_ to accomplish this is to actually implement a
math library, get people to use it, and then propose its integration."

The main advantage I see for the "easy maths" library as I would like to
propose/contribute to is the easiness of use through a well thought
integration to the swift language... I don't really see people adopting it
massively if it is neither very performant nor very easy to use and this is
the reason why I think that point #1 is a good place to start (as long as
at least an working implementation is available behind each of the
functionalities offered). Even if later on, the whole low-level
implementations would be replaced by code from other libraries, having
defined a good syntax for integration with the rest of the language will
IMO not have been be a waste of time.

Point #1 is something that I have not seen anywhere yet in swift, but I
guess that the issue is that we need to experiment with the grammar to work
on the syntax that programmers using that library would experience (is that
ugly? clear? confusing? overloaded? expressive? compact enough?)
  => I do not know how to experiment with the grammar but would like to
learn how to do that: *can someone point me to things I should read and/or
to the places I should make my changes in order to change the swift grammar
and to begin to experiment with it locally on my install?*

2) and 3) is typically what I could see while looking at Surge / Upsurge on
github. There might be more to do here though but I have not took time to
really check, what definitely seems missing to me are linear algebra
algorithms (I mean beyond algorithmic and the call to the existing
functions of the Accelerate Framework). The good news here is that I could
definitively fill some/most of these gaps (I am just not sure that I should
start by adding this to libraries that are based on the Acceleration
Framework since I am rather interested in a multiplatform "easy maths"
swift library)

4) is where the actual magic would happen: take a super natural and easy to
read code and make it quite fast. This can only be made based on the
results of the other 3 points since the expressions to understand come from
#1 and the options provided for optimization come from #2 and #3.

Quite OT for the current discussion, but I would also want to bring a
collection of optimization algorithms and ODE solvers to the "easy maths"
library. Nothing very complicated actually but it is always appreciable to
have them directly available when you need one to test an idea that
requires one, instead of writing your own or adding a third party library
to your project just for the test.

Nicolas

···

On Fri, Feb 17, 2017 at 8:25 PM, Stephen Canon via swift-evolution < swift-evolution@swift.org> wrote:

On Feb 17, 2017, at 10:51 AM, Xiaodi Wu via swift-evolution < > swift-evolution@swift.org> wrote:

There's nothing, afaik, which stands in the way of that syntax today. The
proposal is to extend the standard library to add syntax for a math
library. The idea of having a core math library has already been mentioned
on this list, to great approval, but it should come in the form of an
actual library, and not a syntax only!

Right. IMO the way to do this is to develop such a library outside of the
stdlib to the point that it’s useful enough to be employed for real work
and people can evaluate what works and what doesn’t. That may require small
language and stdlib changes for support, which should be brought-up here.

Once such a library were reasonably mature, it would be reasonable to
propose it for inclusion in swift proper. I expect this process will take a
couple *years*.

FWIW, I personally despise MATLAB .operator notation, though I accept that
it’s pretty much achieved saturation at this point. It looks reasonably
nice for simple single-operand expressions, but it imposes a large burden
on the compiler (and often leads to inefficient code). In this regard, they
are something of an attractive nuisance. Consider your example:

3.0 .* A .* B ./ (C.^ 4.0)

with the most obvious implementation, this generates four calls to vector
functions:

- multiply array by scalar (tmp0 <— 3 .* A)
- elementwise multiplication (tmp1 <— tmp0 .* B)
- elementwise exponentiation (tmp2 <— C .^ 4)
- elementwise division (result <— tmp1 ./ tmp2)

again, with the obvious implementation, this wastes space for temporaries
and results in extraneous passes through the data. It is often *possible*
to solve these issues (at least for some the most common cases) by
producing proxy objects that can fuse loops, but that gets very messy very
fast, and it’s ton of work to support all the interesting cases.

On the other hand, the stupid obvious loop:

for i in 0 ..< count {
result[i] = 3*A[i]*B[i]/(C[i]*C[i]*C[i]*C[i])
}

or the cleaner, with a little sugar:
zip3(A,B,C).map { 3 * $0.0 * $0.1 / ($0.2 ^ 4) }

requires a tiny bit of boilerplate, but only a single pass through the
data and allows the compiler to vectorize. Even if the four vector
functions use by the .operations are perfectly hand-optimized, the multiple
passes and extra memory traffic they entail often makes it *slower* than
the stupid for loop.

I don’t mean to be too discouraging; all of these issues are surmountable,
but I (personally) think there’s a lot of development that should happen
*outside* of the stdlib before such a feature is considered for inclusion
in the stdlib.

– Steve

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Xiaodi Wu) #10

> If you're simply looking for elementwise multiply without performance
> requirements, map(*) is a very succinct spelling.

Yes, it is (combined with zip), but:
1) zip map will not enforce same size (which shall be done to fail hard
early), nor allow to combine with an array of a single element, nor with a
single element (same type but not even wrapped in the array)
2) the syntax becomes a bit uglier if you want an element-wise
multiplication of two [[[Double]]]
3) And the syntax I propose allow scientist to write things like (here
Version 2)

    3.0 .* A .* B ./ (C.^ 4.0)

which is extremely clear to read and look pretty much as we would write in
maths. BTW some of the functionalities I am mentioning here are existing
with no specific libraries with the std::valarray in C++.

What you're asking for is a Swift-native math library! This is something
that people are interested in making, and I would want one too.

> Performant implementations for these operations like you have in Matlab
rely
> on special math libraries. Apple platforms have Accelerate that makes
this
> possible, and other implementations of BLAS/LAPACK do the same for Linux
and
> Windows platforms.
>
> There has been talk on this list of writing Swifty wrappers for such
> libraries. The core team has said that the way to get such facilities
into
> Swift corelibs is to write your own library, get broad adoption, then
> propose its acceptance here. Currently, several libraries like Surge and
> Upsurge offer vectorized wrappers in Swifty syntax for Apple platforms;
it
> would be interesting to explore whether the same can be done in a
> cross-platform way.
>
> But simply adding sugar to the standard library will not give you the
> results you're looking for (by which I mean, the performance will be
> unacceptable), and there's no point in providing sugar for something that
> doesn't work like the operator implies (Matlab's elementwise operators
offer
> _great_ performance).
>

Yes this is clear to me. My proposal is rather on bringing these syntax to
the language and the code given is only here to illustrate the kind of
behavior that I desire. Whether the computations are delegate to external
libraries, etc. is something that is very important for the performance but
that *should happen behind the scene* and which is IMO relatively
decoupled from the syntax exposed to the programmers.

Yes, what you are describing is the syntax for a Swift math library. But
you're saying you're interested in designing only the syntax but not the
implementation! Swift Evolution proposals that are accepted actually get
implemented: someone actually needs to make things happen behind the scenes.

What I'm saying is that there are people who are actively designing Swift
math libraries. Although it may not look it based on the chains that get
the highest response, bikeshedding syntax isn't what this list is for. A
proposal needs to have a detailed design for implementing a feature, not
just propose a syntax. Elementwise operators need to come with a performant
implementation.

I would like to insist on a particular point which is of daily concern to

me at work, where I cannot really use swift (at least not yet) and do these
kinds of things in C or C++. One issues that we have in academia is that we
often have algorithms developed independently by different groups of people
in different projects and for many years with no intentions to bring them
together.

Team A decides to built a solution based on a dedicated library libA
whereas team B decides to use another library libB for providing these
functionalities. Often it might even be the same library but in completely
different versions because there was 10 years between both developments.
Now a project C started and need to reuse pieces from project A and B but
the libraries do not "talk to each other": a matrix-type from libA is
different from a matrix-type from libB and you must refactor most of the
code to get the whole thing work together.

This situation also never happen (or to a very limited extend) in Matlab,
because the whole basic objects, matrices and so on are directly part of
the language and not provided by external libraries. The call to the highly
performant BLAS/LAPACK libraries happen behind the scenes and in
99.999999999% of the cases you never have to take care of anything. If
swift was integrating the right syntaxes in the language, my guess is that
many scientists who currently use C++ would be very interested (just as
most of them were preferring FORTRAN to C/C++ a long time ago).

Right, this is an excellent argument for a core math library in Swift. And
members of the core team have supported that idea. However, they have
stated that the _method_ to accomplish this is to actually implement a math
library, get people to use it, and then propose its integration.

···

On Fri, Feb 17, 2017 at 12:34 PM, Nicolas Fezans <nicolas.fezans@gmail.com> wrote:


(Abe Schneider) #11

Well I was rather thinking of making a Swift-only library (at least at first) but that would also be available for other platforms e.g Linux or maybe some day on Windows => also working with reduced performance without the Accelerator Framework but leveraging it on Apple Platforms (and possibly leveraging others on the other platforms). This said I am open to discussion on this... but having a very nice syntax for swift and having an close to one-to-one equivalent also for Objective-C will probably add quite some difficulties.

While still very much in its infancy, just to add the libraries out there, there is: https://github.com/abeschneider/stem . However, the library currently suffers from design issues related to dispatching correctly from generic functions. That said, I was able to recreate a large part of the Numpy functionality while allowing the ability to leverage the Accelerator Framework and OpenCL/CUDA.

> again, with the obvious implementation, this wastes space for temporaries and results in extraneous passes through the data. It is often *possible* to solve these issues (at least for some the most common cases) by producing proxy objects that can fuse loops, but that gets very messy very fast, and it’s ton of work to support all the interesting cases.

This is clear to me and to be honest with you I am not really sure of the best strategy to make this.

The most successful method I’ve seen for dealing with this is let the user write what is most natural first (allowing for temporaries) but provide a path to optimize (using in-place operations). While expression trees can automate this for the user, it also has the potential for being much more difficult to debug and may not be as optimal as a hand-crafted expression.

I don't think that the primary target for the library should be to deliver the highest performance possible.
  => People who do need that level of performance would still need to analyze and optimize their code by themselves and/or directly call the Acceleration Framework or other specialized libraries.

What I would like to reach instead is rather what I would call "the highest usability possible with decent performance". Some programmers will be satisfied with the level of performance and will enjoy the readability and maintainability of the code based of the library, whereas other will go for more performant libraries (and that is perfectly fine!). Actually, I would even expect later that some of those who belong to the latter category will start experimenting with the easy but less performant library (lets call it here "easy maths library") and optimize their code based on a high performance library only in a second step.

If you can define your operations at the right granularity you can write really optimized Accelerate/OpenCL/CUDA code for the low level parts and string it together with less optimize code.

My idea of a possibly pragmatic roadmap (which can be followed in that order) to make such a library almost from scratch with the long-term goal of being quite performant but primarily very easy to use could be:

1) think about the integration to the language, the syntax, the high-level user documentation, etc. and demonstrate all this based on a relatively low performance implementation

2) generate a collection a typical operations where the low-level libraries offer very nice performance or where a clever handling of the temporary variables is possible

For (1) and (2), it’s worth taking a look at what libraries exist already. People have spent a lot of time organizing and re-organizing these. While not perfect, Numpy has become one of the most successful matrix libraries out there.