Re: [elixir-core:7343] Product functions for Enumerable

77 views
Skip to first unread message

José Valim

unread,
Jul 28, 2017, 3:58:28 AM7/28/17
to elixir-l...@googlegroups.com
Hi Burdock,

for-comprehensions map conceptually to flat_map (implementation-wise they use reduce though). So the code above could be written as:

    Enum.flat_map([:a,:b,:c], fn i ->
      Enum.flat_map([1, 2], fn j ->
        {i, j}
      end)
    end)

The benefit is that it just works with Stream.



José Valim
Skype: jv.ptec
Founder and Director of R&D

On Fri, Jul 28, 2017 at 2:19 AM, Burdock <dmay...@gmail.com> wrote:
Hello everyone,

One of the most attractive features of list comprehensions is their ability to avoid nested loops
for i <- [:a, :b, :c], j <- [1, 2], do: {i, j} # Happy dev :)

flat_map
([:a, :b, :c],
  fn i
->
    map
([1,2],
      fn j
->
       
{i, j}
     
end)
 
end) # Sad dev :(

Something that I have wanted for a long time is a way to do the same thing as list comprehensions, but with the Enum syntax.
Enum.product([ [:a,:b,:c], [1,2] ])
|> Enum.map( {i, j} -> ... end) # Same tuple syntax as zip

This could be extended further by adding nested access like get_in/2 

carts = [
    products
: [
   
%{name: "shirt",
      coupons
: 101, 202
   
},%{name: "pants",
      coupons
: 303, 404
   
}]
 
}, %{
    products
: [
   
%{name: "T-shirt",
      coupons
: 101, 202
   
},%{name: "shorts",
      coupons
: 303, 404
   
}]
 
}
]

Enum.product(carts, [:products, :coupons])
|> Enum.map( {cart, product, coupon} -> ... end) # Every Layer of nesteing is kept available via tuple syntax

# Alternative syntax where Enum.product/2 :: left, right -> Enum.product([left, right]) would work

Enum.product(carts, :products)
|> Enum.product(:coupons)
|> Enum.map( {cart, product, coupon} -> ... end) # Every Layer of nesteing is kept available via tuple syntax

This is cool and all, but what about Stream.product? Unfortunately, Streams are not quite as simple. Streams don't always have a finite length.
natrual_numbers = Stream.unfold(1, fn n -> n + 1 end)
for i <- natrual_numbers, j <- natrual_numbers, do: {i, j} # ????????

I don't want to get into too much depth on Stream.product here because making it work safely with Streams of unknown length requires a somewhat math heavy explanation. If we get the green light on Enum.product I will make the suggestion for Stream.product. The only thing that's important is that it is possible via some higher dimensional pairing function magic :)

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/ef5b1679-d956-4ba2-b18f-1b63d1dcdf3a%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Burdock

unread,
Jul 28, 2017, 1:21:47 PM7/28/17
to elixir-lang-core, jose....@plataformatec.com.br
Hi José,

While the general concept of for-comprehensions is flat_map, the inner most function usually is not. This is because you usually don't want to flatten your values, just the surrounding lists. The previous example will throw an undefined protocol error if the inner map is flat_map because it will try to flatten the tuple {i,j}.  

While I am focusing on Enum.product for this first post, it is mostly just a precursor to Stream.product. This isn't exactly a proposition exclusive to Elixir, but a general Stream idea/function that I and a few others have worked on. Building heavily on Matthew Szudzik's "elegant" version of Cantor's pairing function.

The issue with getting the cartesian product (nested iteration) of Streams the default way is that it's unsafe. If the inner Stream doesn't halt, the outer Stream doesn't increment. This means that any number of valid Streams become invalid due to improper enumeration order. 

natrual_numbers = Stream.unfold(1, fn n -> n + 1 end)

Stream.product(natrual_numbers, natrual_numbers, natrual_numbers) # a, b ,c
|> Stream.filter(fn {a,b,c} -> a*a + b*b == c*c end) # is a right triangle
|> Stream.take_while(fn {a,b,c} -> (a*b)/2 <= max_area end) # has an area less than max_area

Using the nieve approach, like how for works, you end up just enumerating the last enumerable, [{0,0,1}, {0,0,2}, {0,0,3} .... {0,0,9999999} ...]. Despite trying to create a Stream that will produce every combination of 3 natrual_numbers, you end up with the same stream you started with. Just two 0's tacked on the front. 

This means normal product functions are unsafe with Streams. If any Stream (other than the outer most loop) doesn't halt, the new Stream has inaccessible values. This is the same issues as concat-ing two Streams. If the 1st doesn't halt, every value in the 2nd is inaccessible. 

But there is a way to create the product of N Streams that always safe, and still ordered*. That's the main point of Stream.product. Not only adding safety but allowing you to use Streams is a few new ways. 

At this point, I just wanted to focus on the simpler part of this proposal to get the syntax nailed down. But if Enum.product seems redundant, I will move on to the meat of things: Stream.product. 
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages