[RFC] Introducing with

1,791 views
Skip to first unread message

José Valim

unread,
Oct 21, 2015, 9:20:32 PM10/21/15
to elixir-l...@googlegroups.com
Note: Proposal can also be read on gist. Please do not drop comments on Gist, the discussion should happen on this thread.

`with` is `for`'s younger brother. Imagine we have two functions:

    def ok(x), do: {:ok, x}
    def error(x), do: {:error, x}

While `for` is used to match on values out of a collection:

    for {:ok, x} <- [ok(1), error(2), ok(3)], do: x
    #=> [1, 3]

`with` is used to match on values directly:

    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do: {:ok, x + y}
    #=> {:ok, 3}

Because all values matched, the `do` block was executed, returning its result. If a value does not match, it will abort the chain:

    with {:ok, x} <- ok(1),
         {:ok, y} <- error(2),
         do: {:ok, x + y}
    #=> {:error, 2}

Since `error(2)` did not match `{:ok, y}`, the `with` chain aborted, returning `{:error, 2}`. 

There are many different scenarios on every day Elixir code that we can use `with`. For example, it is useful to avoid nesting "case"s:

    case File.read(path) do
      {:ok, binary} ->
        case :beam_lib.chunks(binary, :abstract_code) do
          {:ok, data} ->
            {:ok, wrap(data)}
          error ->
            error
        end
      error ->
        error
    end

Can now be rewritten as:

    with {:ok, binary} <- File.read(path),
         {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
         do: {:ok, wrap(data)}

Another example is Plug itself. Plug will only call the next plug if the `:halted` field in the connection is false. Therefore a whole plug pipeline can be written as:

    with %{halted: false} = conn <- plug1(conn, opts1),
         %{halted: false} = conn <- plug2(conn, opts2),
         %{halted: false} = conn <- plug3(conn, opts3),
         do: conn

If any of them does not match, because `:halted` is true, the pipeline is aborted.

Similarly to `for`, variables bound inside `with` won't leak. Also similar to `for`, `with` allows "bare expressions". For example, imagine you need to calculate a value before calling the next match, you may write:

    with {:ok, binary} <- File.read(path),
         header = parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)}

That's pretty much the gist of it. Thoughts?

## FAQ

Q: Why `with` instead of one of the operators proposed on other discussions?

The operators in the other discussions were always bound to some particular shape. For example, it would always match on `{:ok, _}`, which is limitting. `with` explicitly lays out the pattern, which means you can match on any value you want.

Q: Will `with` work with the pipe operator?

The answer is no. with, for, the pipe operator are implementations of different monads and we know from other comunities monads do not compose well. Even without a type system, the only way to introduce `with` that works with pipe is by defining something like `withpipe`, and maybe `withfor`, which obviously wouldn't scale because there are too many combinations. Furthermore, we really appreciate that `with` makes all the patterns explicit, instead of hiding it behind a pipe operator. `with` also gives us full control on how the arguments are forwarded.

## José goes crazy

To mimic my ElixirConf keynotes, where the talk starts with reasonable proposals and ends-up with me babbling stuff that may not ever see the day of light, let's do it a bit in written form too. :)

In 2014, I had proposed `stream for` and `parallel for` support alongside `for` comprehensions. Could we have similar modifiers for `with` too? I am glad you asked!

We could at least introduce `async with`:

    async with part1 <- fetch_part("foo"), # async
               part2 <- fetch_part("bar"), # async
               {:ok, page1} <- fetch_page(part2), # await part2, async
               {:ok, page2} <- fetch_page(part1), # await part1, async
               do: {page1, page2}

The right side of `<-` is always executed inside a new task. As soon as any of the parts finish, the task that depends on the previous one will be resolved. In other words, Elixir will solve the dependency graph for us and write this in the most performant way as possible. It will also ensure that, if a clause does not match, any running task is cancelled.

That's not the only example. Ecto could introduce `transactional with`, where we wrap the whole `with` chunk in a transaction and rollback if it does not match. Imagine you want to introduce a `User` model followed by a `Log`, here is how it would be written today:

    user_changeset = User.changeset(%User{}, params)
    Repo.transaction fn ->
      case Repo.insert(user_changeset) do
        {:ok, user} ->
          log_changeset = Log.changeset(%Log{user_id: user.id}, params)
          case Repo.insert(log_changeset) do
            {:ok, log} -> {user, log}
            {:error, log_changeset} -> Repo.rollback log_changeset
          end
        {:error, user_changeset} ->
          Repo.rollback user_changeset
    end

Now with `transactional with`:

    user_changeset = User.changeset(%User{}, params)
    transactional with user <- Repo.insert(user_changeset),
                       log_changeset = Log.changeset(%Log{user_id: user.id}, params),
                       log <- Repo.insert(log_changeset),
                       do: {user, log}

Note: we can't use `transaction` as a name because it is already part of the Ecto.Repo API. If someone knows a shorter word than `transactional`, I would appreciate it. Please send me a personal e-mail. :)

You can see a discussion about this Ecto example in particular here: https://github.com/elixir-lang/ecto/issues/1009. Of course, there is a lot of prior art on all of this, but those last two examples, async and transactional, are heavily inspired by computation expressions from F#.

While this "José goes crazy" section is meant to highlight how we could extend this feature, let's focus on the first part of this issue: the barebone `with`. It is definitely useful enough to stand on its own.



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

Chris McCord

unread,
Oct 21, 2015, 9:43:38 PM10/21/15
to elixir-l...@googlegroups.com
This looks fantastic! I think I have been so caught up with the pipeline discussions that I never stopped to think about how we might solve the pipe issues… without the pipe. Beautiful. My only thoughts are I wonder if folks will be unsure whether a pipeline of transforms, or with matches suits their problem, but I think it’s clear enough and some level of cross-over like for/Enum is fine. Even for the simple nested cases you mentioned, I think this is a huge win. Something like `transactional` in the future would also make the rollback cases beautiful, which is something that has always felt a little kludgy to me.

--
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-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4J5t8b7RGX3JxBJT-zG%3DKAwO4f6A0UfgZXLu2Q%3DVkNJVg%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Josh Adams

unread,
Oct 21, 2015, 9:46:06 PM10/21/15
to elixir-l...@googlegroups.com
I agree, this looks great. It cleans up a lot of code and reading the
Ecto issue mentioned was also enlightening. Without the crazy bits at
the end, it's already good. Very happy to see new high-level
constructs like this in the distance! :) +1
> https://groups.google.com/d/msgid/elixir-lang-core/11774198-131A-4054-902C-A5A09A3ACCFE%40chrismccord.com.
>
> For more options, visit https://groups.google.com/d/optout.



--
Josh Adams

Gabriel Jaldon

unread,
Oct 21, 2015, 10:24:04 PM10/21/15
to elixir-lang-core, jose....@plataformatec.com.br
This is great! It would definitely clean up a lot of case statements. Explicitly setting the pattern for each function in the pipeline makes it flexible and clarifies the intent of the author. Awesome! 

One thing though, I think I like `when` more than `with`. Using `when` would be like reading 'when pattern matches output of function, do: result'.

Peter Hamilton

unread,
Oct 21, 2015, 11:07:12 PM10/21/15
to elixir-lang-core, jose....@plataformatec.com.br
So to be clear, the proposal is to add `with` as a keyword along to the compiler (with the same rules as `for`) along with an implementation?

Unless I'm mistaken, there's nothing here that couldn't be implemented right now as a `when with` macro (`when with` is needed instead of just `with` or `when` because of compiler... reasons?). There's definitely value in this being added as a keyword and part of mainstream elixir, but would a `when with` implementation be useful to start playing with now?

--
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-co...@googlegroups.com.

Bryan Joseph

unread,
Oct 21, 2015, 11:19:29 PM10/21/15
to elixir-lang-core, jose....@plataformatec.com.br
I like this. I got a bit scared when thinking about "with" in other languages, but this looks great!

I do like Gabriel's suggestion of "when" instead of "with", but that's just splitting hairs. Either way, this is good.

Lance Halvorsen

unread,
Oct 21, 2015, 11:23:56 PM10/21/15
to elixir-l...@googlegroups.com, José Valim
I'm a solid +1. Any language construct that can eliminate nesting so easily and so effectively has my vote.
.L

--
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-co...@googlegroups.com.

Eric Meadows-Jönsson

unread,
Oct 21, 2015, 11:36:06 PM10/21/15
to elixir-l...@googlegroups.com
So to be clear, the proposal is to add `with` as a keyword along to the compiler (with the same rules as `for`) along with an implementation?

Technically it's not a keyword, it's a special form. The distinction is important for the next part.

Unless I'm mistaken, there's nothing here that couldn't be implemented right now as a `when with` macro (`when with` is needed instead of just `with` or `when` because of compiler... reasons?). There's definitely value in this being added as a keyword and part of mainstream elixir, but would a `when with` implementation be useful to start playing with now?

`when` cannot be used because it's a binary operator in use today for guards. The reason `some_macro with ...` is needed is because macros and functions have a fixed arity. Special forms (basically macros implemented in erlang in the elixir compiler) do not have this restriction, that's why a language addition is needed instead of just adding a macro to the standard library.

You can definitely start playing around with an implementation today.

Greg Vaughn

unread,
Oct 21, 2015, 11:50:37 PM10/21/15
to elixir-l...@googlegroups.com
Thumbs up from me. I'd listen to José babble any day!

-Greg
> --
> 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-co...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAM_eapjLVXX7JOv7tLG6kUq7BMa6pyKpw1xjfbw8%2B0m67t8swA%40mail.gmail.com.

Paul Schoenfelder

unread,
Oct 21, 2015, 11:55:34 PM10/21/15
to elixir-l...@googlegroups.com
I really like this as well, especially how cleanly it could be extended like in your example with `async` and `transactional`. +1!

Paul

Petrică Clement Chiriac

unread,
Oct 22, 2015, 1:15:13 AM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
I think `transactional with` it is wrong, correct version is: 

    user_changeset = User.changeset(%User{}, params)
    transactional with 
                       {:ok, user} <- Repo.insert(user_changeset),
                                 log_changeset = Log.changeset(%Log{user_id: user.id}, params),
                       {:ok, log} <- Repo.insert(log_changeset),
                       do: {:ok {user, log}}

please correct me if I'm wrong

Petrica Clement Chiriac

José Valim

unread,
Oct 22, 2015, 5:13:38 AM10/22/15
to Petrică Clement Chiriac, elixir-lang-core
Yes, you are correct. :) I also think we should call it "transact with".



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

X4lldux

unread,
Oct 22, 2015, 6:04:03 AM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
Great idea! But I think for some simple cases it's over verbose. There will be many cases where we will just need simple `{:ok, value}` / `{:error, msg}` pattern and we would need to write something like this:

with {:ok, a} <- get_record(some_id),
     {:ok, b} <- process_record(a),
{:ok, c} <- update_some_fields(b)
     
{:ok, d} <- process_some_more(c),
     do: {:ok, d}

All of this could be written as a simple pipeline:
d = get_record(some_id)
|=> process_record
|=> update_some_fields
|=> process_some_more

`|=>` is hypothetical `{:or, value}` / `{:error, msg}` aware pipe operator. Maybe it could be a macro that translates the shorter version `|=>` to the `with` version.

José Valim

unread,
Oct 22, 2015, 6:50:33 AM10/22/15
to X4lldux, elixir-lang-core

Great idea! But I think for some simple cases it's over verbose. There will be many cases where we will just need simple `{:ok, value}` / `{:error, msg}` pattern and we would need to write something like this:

Yes, I agree. But we have decided exactly to go with something explicit and extensible than choose a single operator that works only with one shape. Folks are welcome to use their own operators (and they have in another thread) but I would like Elixir's to be as straight-forward as possible. We have no plans to introduce a specific operator for this in Elixir itself. :)

Michael Schaefermeyer

unread,
Oct 22, 2015, 9:08:08 AM10/22/15
to elixir-lang-core, xal...@gmail.com, jose....@plataformatec.com.br
This looks absolutely great. Better than what I was hoping for! I was following the different discussions around this with great interest. As some of you will know I am a sucker for readable, eye-pleasing code and that's why I hate nesting.

That also brings me to the one critique I have: I am afraid that with (especially in combination with other special forms such as transactional) will be harder to read because of the indentation. Our eyes are more used to fixed-with tabbed indentation and this looks weird to me:

transact with user <- Repo.insert(user_changeset),
              log_changeset = Log.changeset(%Log{user_id: user.id}, params),
              log <- Repo.insert(log_changeset),
              do: {user, log}

Any way we can make this intend a little nicer? Or define style guides around this? 

Another issue is that using a different keyword than "transact" later on will pollute the git changelog quite a bit cause I'd have to touch ever line that's indented.

This might be a stupid example as it clashes with how do: is normally used but I'd much prefer something along these lines:

with(transact) do
  {:ok, user} <- Repo.insert(user_changeset),

  log_changeset
= Log.changeset(%Log{user_id: user.id}, params),

 
{:ok, log} <- Repo.insert(log_changeset)
finally
  {user, log}
end

That's all. Absolutely love that we are getting something like this!

Thanks José

Drew Olson

unread,
Oct 22, 2015, 9:20:29 AM10/22/15
to elixir-l...@googlegroups.com, xal...@gmail.com, José Valim
This looks fantastic. I'd also echo Michael Schaefermeyer's suggestion of thinking about an alternative syntax that's a bit more "block based".

Perhaps we could take a similar approach to the way we can define functions with either `def foo(a), do: a + 1` or `def foo(a) do a+1 end`.

Very excited for this.

--
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-co...@googlegroups.com.

X4lldux

unread,
Oct 22, 2015, 10:30:38 AM10/22/15
to elixir-lang-core, xal...@gmail.com, jose....@plataformatec.com.br
You're right but the same can be said about thisexplicit form:
res1 = f1()
res2
= f2(res1)
res3
= f3(res2)
res4
= f4(res3)
res4

and yet we have this shortcut:
f1
|> f2
|> f3
|> f4


Why is that much different from having this:
f1
|=> f2
|=> f3
|=> f4
and it's explicit form:
with {:ok, res1} <- f1
     
{:ok, res2} <- f2(res1)
     
{:ok, res3} <- f3(res2)
     
{:ok, res4} <- f4(res3)
     
:do { res4 }

Since we have a built-in form `|>` which solves a very common and yet very specif situation, where one result is passed down a as a first argument, then why not have similar operator that does something  but is expanded as a safer version using `with` and `{:ok, res }`?
I'm not say this hypothetical pipe operator `|=>` should be The One and Only proposed answer to entire problem, but that we should have a this  syntactic sugar like `|>` is right now!
Both types of those explicit expression can can be viewed as a lexically ordered continues operations (i'm not using word "serially" because with `async` keyword, the might not be serially executed).
There is an analogy between them:
with {:ok, res1} <- f1         |   res1 = f1()    
     
{:ok, res2} <- f2(res1)   |   res2 = f2(res1)
     
{:ok, res3} <- f3(res2)   |   res3 = f3(res2)
     
{:ok, res4} <- f4(res3)   |   res4 = f4(res3)
     
:do { res4 }              |   res4

f1       |   f1
|=> f2   |   |> f2
|=> f3   |   |> f3
|=> f4   |   |> f4


So, if syntactic sugar `|>` is there to make our lives easier with one common pattern, there should be something similar in Core Elixir for also a common pattern.

José Valim

unread,
Oct 22, 2015, 11:04:15 AM10/22/15
to X4lldux, elixir-lang-core

You're right but the same can be said about thisexplicit form:
res1 = f1()
res2
= f2(res1)
res3
= f3(res2)
res4
= f4(res3)
res4


I don't think this is an apt comparison. When we use the pipe operator, we do a single assumption:

1. the left side is going to be passed as first value to the right side

The proposed |=> makes many more:

1. the left side must match the tuple {:ok, _}
2. the second element of the tuple is going to be passed as first value to the right side
3. in case of no match, the non matching value is returned

So the number of things being hidden (and therefore not being explicit about) in |=> are many more. In contrast, with/1 is making only one assumption, which is the third one: "in case of no match, the non matching value is returned"




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



 
and yet we have this shortcut:

José Valim

unread,
Oct 22, 2015, 11:07:10 AM10/22/15
to Michael Schaefermeyer, elixir-lang-core, Paweł Drygas
Thanks Michael!

I would like to avoid the block form because it gives folks the impression they can write block expressions (and they would be able to, as a matter of fact):

with transact do
  {:ok, user} <- Repo.insert(user_changeset)
  if this and that do
    ...
    ...
  end
  {:ok, log} <- Repo.insert(log_changeset)
end

I prefer the with because it mirrors for and forces you to break your code into small functions so you can focus on the return values:

transact with user <- Repo.insert(user_changeset),
              log_changeset = Log.changeset(%Log{user_id: user.id}, params),
              log <- Repo.insert(log_changeset),
              do: {user, log}

I agree the styling is an issue though. What if we use the same technique we do with Ecto queries?


transact with(
  user <- Repo.insert(user_changeset),
  log_changeset = Log.changeset(%Log{user_id: user.id}, params),
  log <- Repo.insert(log_changeset),
  do: {user, log}
)

Better? Thoughts?



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

easco

unread,
Oct 22, 2015, 11:33:36 AM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
What is the scope of variables bound by the statement?  


    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do: {:ok, x + y}
    #=> {:ok, 3}

Are x and y only bound within the scope of the 'with' expression and it's "do:" clause (making this something like a "let-do" statement from other languages, or are x and y bound in the enclosing scope?

How about an "otherwise:" kinda similar to an "if-else"

with {:ok, x} <- ok(1),
       {:ok, y} <- error(2),
      do: {:ok, x + y},
      otherwise: fn({:error, value}) -> {:error, value * 5 } end

easco

unread,
Oct 22, 2015, 11:40:16 AM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
Playing devil's advocate to my own ill-conceived thought:

'otherwise' is problematic because the return type of any individual clause of the 'with' can have a different return type:

def biggerError(3, 4) do
 
{:error, 3, 4}
end


with {:ok, x} <- ok(1)
     
{:error, a, b} <- biggerError(2,3)
     
do: {:ok, x, a, b},
     otherwise
: <fn inserted here>


Now you've got a "problem" because fn has to deal with the fact that the matches in the "with" can return a 2 element tuple, or a 3 element tuple.  That means a multi-clause otherwise function, or one that takes a single argument and deals with the fact that the argument could be of any type.  That's not an insurmountable problem, but it is an interesting complication.

Tallak Tveide

unread,
Oct 22, 2015, 12:40:03 PM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
 Such great fun just to follow the evolution of Elixir. Like before Jose, you just interrupt the discussion of monads and pipe operator variations with a completely different, and quite obivously good suggestion. We could all have come up with that particular solution, but you were the one to do it :)

Before I start any criticism, let me just make it clear that I really like the suggested change. It seems quite similar to Haskell's let .. in construct.

Now I haven't had the time to digest this properly, but my critisism would be that the formatting is less readable compared to the pipe that we like so much. The `do` block seems a bit redundant for many uses. I think if we were to keep the piping, we only needed to have pattern filters along the pipes:

# using with

with {:ok, binary} <- File.read(path),
         {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
         do: {:ok, wrap(data)}

# using pipe patterns

File.read(path)
|> [{:ok, &}] :beam_lib.chunks(:abstract_code))
|> [{:ok, &}] wrap

I realize that this particular syntax is full of issues, just food for thought...

Peter Hamilton

unread,
Oct 22, 2015, 12:51:20 PM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
What's the difference between:

    with {:ok, binary} <- File.read(path),
         header = parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)}

and

    with {:ok, binary} <- File.read(path),
         header <- parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)}

?

--
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-co...@googlegroups.com.

José Valim

unread,
Oct 22, 2015, 1:12:59 PM10/22/15
to elixir-l...@googlegroups.com
What's the difference between:

    with {:ok, binary} <- File.read(path),
         header = parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)}

and

    with {:ok, binary} <- File.read(path),
         header <- parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)} 

In this particular case, none. :) It would be different for async though because the right side of <- would always happen in task.
 

Peter Hamilton

unread,
Oct 22, 2015, 1:48:20 PM10/22/15
to elixir-l...@googlegroups.com
One behavioral difference I can think of might be that `{:ok, header} = parse_header(binary)` would raise MatchError if it fails whereas `{:ok, header} <- parse_header(binary)` returns the result of `parse_header(binary)`.

--
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-co...@googlegroups.com.

Scott Thompson

unread,
Oct 22, 2015, 1:50:10 PM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
That was something I noted about this syntax.  Both the "=" and the "<-" seem to be match operators.  The difference might be that "<-" upon failing a match causes the with statement to return the mismatched value as the value of the with expression overall while using "=" and leading to a mismatch would cause a runtime mismatch exception.

Seems like a subtle difference.

Scott

José Valim

unread,
Oct 22, 2015, 1:50:58 PM10/22/15
to elixir-l...@googlegroups.com
One behavioral difference I can think of might be that `{:ok, header} = parse_header(binary)` would raise MatchError if it fails whereas `{:ok, header} <- parse_header(binary)` returns the result of `parse_header(binary)`.

Yes, I was commenting about that example in particular, sorry. :) Also, this exactly same distinction already happens in "for" today: <- is a match but you can get hard matches by writing "x <- ..., {:ok, v} = x".

José Valim

unread,
Oct 22, 2015, 1:51:40 PM10/22/15
to Scott Thompson, elixir-lang-core

That was something I noted about this syntax.  Both the "=" and the "<-" seem to be match operators.  The difference might be that "<-" upon failing a match causes the with statement to return the mismatched value as the value of the with expression overall while using "=" and leading to a mismatch would cause a runtime mismatch exception.

Seems like a subtle difference.

Yes, precisely. And as I just replied to Peter, this distinction already happens inside "for". :)

X4lldux

unread,
Oct 22, 2015, 2:04:17 PM10/22/15
to elixir-lang-core, xal...@gmail.com, jose....@plataformatec.com.br
I would say you're right and you're wrong.

They might be different from that perspective, but not from point of usability or the conceptual/abstract operation they both do.
I mean, |> is a value operator where a |=> would be a monadic operator. In this view, both have exactly same number of assumptions.


Additionally, I believe your argument could be applied to `unless` also:
1) evaluate predicate
2) inverse the result of predicate
3) execute :do or :else block

or in a way that has only one less step:
1) evaluate predicate
2) execute :else or :do block

(although, in this interpretation the inversion is still there, just hidden)


I understand the need for being explicit, but for this particular pattern it will be an over kill and we will sacrifice readability and usability.
This topic, handling errors in pipelines, I believe, is the 3rd most common brought up subject in "Elixir articles realm", loosing only to phoenix and ecto articles - so I think there is the need for this type of behavior in community. And also understanding of what and how those values will be wrapped in `{:ok, _}` monad. While in the end, this bring us to safer version of one of the best code-readability-enhancing feature of Elixir - the pipe |> operator :)

Scott Thompson

unread,
Oct 22, 2015, 2:20:21 PM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
I noticed the similarity to let...in too.  It also comes up, I think in ML, Clojure and a number of other languages.

So it doesn't get lost, because I tied it to another idea:

What is the scope of variables bound by the statement?  
    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do: {:ok, x + y}
    #=> {:ok, 3}

Are x and y only bound within the scope of the 'with' expression and it's "do:" clause (making this something like a "let-do" statement from other languages, or are x and y bound in the enclosing scope?


With Elixir, I think I would expect the values of x and y to be bound past the with statement, but then what's the value of y  if the x match succeeds and the y match doesn't.

José Valim

unread,
Oct 22, 2015, 2:22:28 PM10/22/15
to Scott Thompson, elixir-lang-core
What is the scope of variables bound by the statement?  


    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do: {:ok, x + y}
    #=> {:ok, 3}

Are x and y only bound within the scope of the 'with' expression and it's "do:" clause (making this something like a "let-do" statement from other languages, or are x and y bound in the enclosing scope?

It is the same as "for", nothing will leak the "with" scope.

Scott Thompson

unread,
Oct 22, 2015, 2:24:54 PM10/22/15
to elixir-lang-core, eas...@gmail.com, jose....@plataformatec.com.br
Oh right... 'with' will likely be a macro and we have hygienic macros.  Hadn't thought of it from that perspective.

Drew Olson

unread,
Oct 22, 2015, 2:34:10 PM10/22/15
to elixir-l...@googlegroups.com, eas...@gmail.com, José Valim
Perhaps `yield` is a better term than `do` for this feature (it's the term used by Scala, for example). Not sure if this would be inconsistent with other features, but `do` seems "special" in this one.

--
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-co...@googlegroups.com.

Scott Thompson

unread,
Oct 22, 2015, 2:45:22 PM10/22/15
to elixir-lang-core, eas...@gmail.com, jose....@plataformatec.com.br, dr...@drewolson.org
My personal preference is for "do". Yield already means "this process is willing to give up the remainder of it's time on the scheduler and allow some other process to run" and my preference would be not to confuse that.

Tymon Tobolski

unread,
Oct 22, 2015, 3:24:34 PM10/22/15
to elixir-lang-core, eas...@gmail.com, jose....@plataformatec.com.br, dr...@drewolson.org
Hi, this seems really great!

Just my two cents: could it be possible to somehow reuse the "for" keyword in a non conflicting way? "with" might be a bit confusing (it has very different meaning in e.g. c#) plus I found it pretty nice to be used in libraries - https://github.com/monterail/tesla ;)

One way thing that is implemented in scala is something like this:

for {
  (x,y) <- listOfTuples
  c = x+y
} yield c

so there is a difference between bind/foldLeft (<-) and the match/assignment (=). 
In Elixir we have <- in for as bind-ish and the same <- used for pattern matching which might be a bit confusing.

I don't have any superb idea how to solve it, just wanted to point this out.

Jim Freeze

unread,
Oct 22, 2015, 3:38:19 PM10/22/15
to elixir-l...@googlegroups.com
Some (probably obvious) questions:


Would the following formatting be permitted?

with
  %{halted: false} = conn <- plug1(conn, opts1),
  %{halted: false} = conn <- plug2(conn, opts2),
  %{halted: false} = conn <- plug3(conn, opts3),
do: conn

with
  {:ok, binary} <- File.read(path),
  {:ok, data}   <- :beam_lib.chunks(binary, :abstract_code),
do: {:ok, wrap(data)}


Is every time a <- is seen, is the right hand side performed inside a task, or is that just for async?

with
  x = {:ok, 1} <- ok(1),
do: x
#=> 1

When a method returns a single value (the degenerative case), is the following the outcome?

with
  x <- echo("fred")
do: x
#=> "fred"


# current common use case
data
  |> filter_a
  |> filter_b
#=> filtered data

# common use case but using with
with
  data <- filter_a(data),
  data <- filter_b(data),
do: data
#=> filtered data

# Assuming data is a map that has a done: key
with
  %{done: false} = data <- filter_a(data),
  %{done: false} = data <- filter_b(data),
do: data
#=> Returns either data or the value of the first filter that does not set :done to false

Thanks.

Jim


--
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-co...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
Dr. Jim Freeze, Ph.D.

José Valim

unread,
Oct 22, 2015, 3:45:47 PM10/22/15
to elixir-l...@googlegroups.com
with
  %{halted: false} = conn <- plug1(conn, opts1),
  %{halted: false} = conn <- plug2(conn, opts2),
  %{halted: false} = conn <- plug3(conn, opts3),
do: conn

No, you need parens:

with(
  %{halted: false} = conn <- plug1(conn, opts1),
  %{halted: false} = conn <- plug2(conn, opts2),
  %{halted: false} = conn <- plug3(conn, opts3),
  do: conn
)

Is every time a <- is seen, is the right hand side performed inside a task, or is that just for async?

with
  x = {:ok, 1} <- ok(1),
do: x
#=> 1

Just for async. 
 
# common use case but using with
with
  data <- filter_a(data),
  data <- filter_b(data),
do: data
#=> filtered data

It doesn't make much sense to use "with" if you are not performing any match. If you only assign to a variable, it will always match, which means it will always move to the next expression.
 
# Assuming data is a map that has a done: key
with
  %{done: false} = data <- filter_a(data),
  %{done: false} = data <- filter_b(data),
do: data
#=> Returns either data or the value of the first filter that does not set :done to false

This will likely return %{done: true} or %{done: false} assuming filter_a and friends always returns maps.

Jim Freeze

unread,
Oct 22, 2015, 3:50:25 PM10/22/15
to elixir-l...@googlegroups.com
On Thu, Oct 22, 2015 at 2:45 PM, José Valim <jose....@plataformatec.com.br> wrote:


It doesn't make much sense to use "with" if you are not performing any match. If you only assign to a variable, it will always match, which means it will always move to the next expression.

Right. I'm just looking at the degenerative case to see what it looks like. 
 
# Assuming data is a map that has a done: key
with
  %{done: false} = data <- filter_a(data),
  %{done: false} = data <- filter_b(data),
do: data
#=> Returns either data or the value of the first filter that does not set :done to false

This will likely return %{done: true} or %{done: false} assuming filter_a and friends always returns maps.

If all filters return %{done: false}, then the do: data will dictate that data should be returned, correct?

Jim
 

--
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-co...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
Oct 22, 2015, 4:13:21 PM10/22/15
to elixir-l...@googlegroups.com
If all filters return %{done: false}, then the do: data will dictate that data should be returned, correct?

The do part is only invoked if they all match. And, in your case, data is always a map if they match.

Jim Freeze

unread,
Oct 22, 2015, 4:20:32 PM10/22/15
to elixir-l...@googlegroups.com
Just to be clear, if they all match, then the do: data clause will be executed and data will be returned (the full map).

If one of the filters returns %{done: true}, then %{done: false} = data will yield a runtime error?

If that is true, then %{done: true} could never be returned. Correct?

Jim


On Thu, Oct 22, 2015 at 3:12 PM, José Valim <jose....@plataformatec.com.br> wrote:
If all filters return %{done: false}, then the do: data will dictate that data should be returned, correct?

The do part is only invoked if they all match. And, in your case, data is always a map if they match.

--
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-co...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
Oct 22, 2015, 4:35:23 PM10/22/15
to elixir-l...@googlegroups.com
Just to be clear, if they all match, then the do: data clause will be executed and data will be returned (the full map).

If one of the filters returns %{done: true}, then %{done: false} = data will yield a runtime error?

No, if the left side of <- does not match, it will return the value that does not match. That's the difference of <- with =, <- will return the non-matching result, aborting the chain!

Onorio Catenacci

unread,
Oct 22, 2015, 4:42:49 PM10/22/15
to elixir-lang-core, jose....@plataformatec.com.br
At the risk of showing people they've been right about my lack of intelligence all along . . . 

    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do 
           {:ok, x + y}
           #other statements
         end

is implied in the proposal right?  I mean do: vs. do  . . . end are no different in this proposal right?  

By the way kudos to all of you for coming up with an elegant solution to a somewhat gnarly design problem. +1

--
Onorio



On Wednesday, October 21, 2015 at 9:20:32 PM UTC-4, José Valim wrote:
Note: Proposal can also be read on gist. Please do not drop comments on Gist, the discussion should happen on this thread.

`with` is `for`'s younger brother. Imagine we have two functions:

    def ok(x), do: {:ok, x}
    def error(x), do: {:error, x}

While `for` is used to match on values out of a collection:

    for {:ok, x} <- [ok(1), error(2), ok(3)], do: x
    #=> [1, 3]

`with` is used to match on values directly:

    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do: {:ok, x + y}
    #=> {:ok, 3}

Because all values matched, the `do` block was executed, returning its result. If a value does not match, it will abort the chain:

    with {:ok, x} <- ok(1),
         {:ok, y} <- error(2),
         do: {:ok, x + y}
    #=> {:error, 2}

Since `error(2)` did not match `{:ok, y}`, the `with` chain aborted, returning `{:error, 2}`. 

There are many different scenarios on every day Elixir code that we can use `with`. For example, it is useful to avoid nesting "case"s:

    case File.read(path) do
      {:ok, binary} ->
        case :beam_lib.chunks(binary, :abstract_code) do
          {:ok, data} ->
            {:ok, wrap(data)}
          error ->
            error
        end
      error ->
        error
    end

Can now be rewritten as:

    with {:ok, binary} <- File.read(path),
         {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
         do: {:ok, wrap(data)}

Another example is Plug itself. Plug will only call the next plug if the `:halted` field in the connection is false. Therefore a whole plug pipeline can be written as:

    with %{halted: false} = conn <- plug1(conn, opts1),
         %{halted: false} = conn <- plug2(conn, opts2),
         %{halted: false} = conn <- plug3(conn, opts3),
         do: conn

If any of them does not match, because `:halted` is true, the pipeline is aborted.

Similarly to `for`, variables bound inside `with` won't leak. Also similar to `for`, `with` allows "bare expressions". For example, imagine you need to calculate a value before calling the next match, you may write:

    with {:ok, binary} <- File.read(path),
         header = parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)}

That's pretty much the gist of it. Thoughts?

## FAQ

Q: Why `with` instead of one of the operators proposed on other discussions?

The operators in the other discussions were always bound to some particular shape. For example, it would always match on `{:ok, _}`, which is limitting. `with` explicitly lays out the pattern, which means you can match on any value you want.

Q: Will `with` work with the pipe operator?

The answer is no. with, for, the pipe operator are implementations of different monads and we know from other comunities monads do not compose well. Even without a type system, the only way to introduce `with` that works with pipe is by defining something like `withpipe`, and maybe `withfor`, which obviously wouldn't scale because there are too many combinations. Furthermore, we really appreciate that `with` makes all the patterns explicit, instead of hiding it behind a pipe operator. `with` also gives us full control on how the arguments are forwarded.

## José goes crazy

To mimic my ElixirConf keynotes, where the talk starts with reasonable proposals and ends-up with me babbling stuff that may not ever see the day of light, let's do it a bit in written form too. :)

In 2014, I had proposed `stream for` and `parallel for` support alongside `for` comprehensions. Could we have similar modifiers for `with` too? I am glad you asked!

We could at least introduce `async with`:

    async with part1 <- fetch_part("foo"), # async
               part2 <- fetch_part("bar"), # async
               {:ok, page1} <- fetch_page(part2), # await part2, async
               {:ok, page2} <- fetch_page(part1), # await part1, async
               do: {page1, page2}

The right side of `<-` is always executed inside a new task. As soon as any of the parts finish, the task that depends on the previous one will be resolved. In other words, Elixir will solve the dependency graph for us and write this in the most performant way as possible. It will also ensure that, if a clause does not match, any running task is cancelled.

That's not the only example. Ecto could introduce `transactional with`, where we wrap the whole `with` chunk in a transaction and rollback if it does not match. Imagine you want to introduce a `User` model followed by a `Log`, here is how it would be written today:

    user_changeset = User.changeset(%User{}, params)
    Repo.transaction fn ->
      case Repo.insert(user_changeset) do
        {:ok, user} ->
          log_changeset = Log.changeset(%Log{user_id: user.id}, params)
          case Repo.insert(log_changeset) do
            {:ok, log} -> {user, log}
            {:error, log_changeset} -> Repo.rollback log_changeset
          end
        {:error, user_changeset} ->
          Repo.rollback user_changeset
    end

Now with `transactional with`:

    user_changeset = User.changeset(%User{}, params)
    transactional with user <- Repo.insert(user_changeset),
                       log_changeset = Log.changeset(%Log{user_id: user.id}, params),
                       log <- Repo.insert(log_changeset),
                       do: {user, log}

Note: we can't use `transaction` as a name because it is already part of the Ecto.Repo API. If someone knows a shorter word than `transactional`, I would appreciate it. Please send me a personal e-mail. :)

You can see a discussion about this Ecto example in particular here: https://github.com/elixir-lang/ecto/issues/1009. Of course, there is a lot of prior art on all of this, but those last two examples, async and transactional, are heavily inspired by computation expressions from F#.

While this "José goes crazy" section is meant to highlight how we could extend this feature, let's focus on the first part of this issue: the barebone `with`. It is definitely useful enough to stand on its own.

Jim Freeze

unread,
Oct 22, 2015, 4:43:26 PM10/22/15
to elixir-l...@googlegroups.com
Ok. I get it. Thank you.

And if the last filter does match with the "=", the chain is completed and the do: clause gets executed and is returned.


--
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-co...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Jim Freeze

unread,
Oct 22, 2015, 4:46:04 PM10/22/15
to elixir-l...@googlegroups.com
Good question. Here's my guess before José responds. No. It looks like a do/end block as an argument inside a function call.

with(
 {:ok, x} <- ok(1),
 {:ok, y} <- ok(2),
do 
  {:ok, x + y}
  #other statements
end
)

--
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-co...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Onorio Catenacci

unread,
Oct 22, 2015, 4:50:30 PM10/22/15
to elixir-lang-core
Well I'm glad to see that question wasn't as dumb as it seemed--or you're very kind!  Actually I asked because I was unsure if the parsing differences between do: and do . . .  end might impact anything.


--
Onorio

José Valim

unread,
Oct 22, 2015, 5:37:05 PM10/22/15
to elixir-l...@googlegroups.com
At the risk of showing people they've been right about my lack of intelligence all along . . . 

    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do 
           {:ok, x + y}
           #other statements
         end

is implied in the proposal right?  I mean do: vs. do  . . . end are no different in this proposal right?  

Yes, your do end version will work just fine, except by the comma before "do".

Jim Freeze

unread,
Oct 22, 2015, 6:26:59 PM10/22/15
to elixir-l...@googlegroups.com
So, the following should work and be equivalent:

with {:ok, x} <- ok(1),
  {:ok, y} <- ok(2)
  do 
    {:ok, x + y}
    #other statements
  end

with \
  {:ok, x} <- ok(1),
  {:ok, y} <- ok(2)
  do 
    {:ok, x + y}
    #other statements
  end

with(
  {:ok, x} <- ok(1),
  {:ok, y} <- ok(2)
  do 
    {:ok, x + y}
    #other statements
  end
)

--
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-co...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Michał Muskała

unread,
Oct 22, 2015, 7:02:29 PM10/22/15
to elixir-l...@googlegroups.com
I really like the with proposal, because it solves the problem we all
have in a great way.

I would like to argue a little bit against additional operators. I
think the fact, that we have so many different proposals shows that
whatever we choose won't be satisfactory. We also tie ourselves to
just one possible success value - the {:ok, value} tuple - this
discussion is, in my opinion, fixed too much on those. The proposed
syntax allows clear handling of other structures, like with the plug
example.

On the other hand we have the hate for verbosity. We're discussing
transactional and async transformers for the with syntax - why woudn't
there be one that would allow ommiting those pesky {:ok, _}, and just
assume this pattern?

matching_ok with(
x <- ok(1),
y <- ok(2),
do: x + y
)

would be equivalent to

with(
{:ok, x} <- ok(1),
{:ok, y} <- ok(2),
do: {:ok, x + y}
)

Finally I would like to explore possible different notations. I really
appreciate the similarity between the proposed with and the for
comprehensions, as one can argue both are implementations of monads
and express similar concepts. Nonetheless, it feels slightly clunky to
me. I would propose going with a simple block returning the last
value, as the final expression's result:

with(transact) do
{:ok, user} <- Repo.insert(user_changeset)
{:ok, log} <- Repo.insert(log_changeset(user))
{:ok, log, user}
end

Such a syntax does not need a special form support from the compiler,
and could even be introduced as a library, there's also no ambiguity
about indentation or splitting lines. It also allows us to pass
modifiers such as transact, async, etc, as argument to the with macro.
I image you could leverage protocols to apply the proper modifier's
behaviour. Actually, I already implemented a basic naïve version in
about 20 minutes: https://github.com/michalmuskala/with. It obviously
lacks good error reporting, etc, but shows how easy it is to implement
the basic concept.

Michael Schaefermeyer

unread,
Oct 22, 2015, 7:14:14 PM10/22/15
to elixir-lang-core
I really like Michał's proposal which is probably not a big surprise given that i suggested something similar and he's got a great first name.

While we all agree that José's suggestion on how to do this (matching explicitly) is by far the best, there remains an issue with the syntax.

While I think

while(
{:ok, match} <- fun1(val),
{:ok, match} <- fun2(val),
do: something
)

Looks better, it's still not ideal. Because parens. For me the for comprehensions is a bit different as it normally doesn't include that many different "arguments" (at least in my experience).

I feel bad nit-picking about syntax when José just wants to give us something awesome. But one of the reasons I'm so passionate about Elixir is that it's so powerful and beautiful.

Thanks

Booker Bense

unread,
Oct 23, 2015, 12:00:59 AM10/23/15
to elixir-lang-core
I really really hate that. Nothing personal, but we already have for which is bad enough, but while just
has too much baggage from imperative languages. It's a different enough construct that it should have
a non-familiar name. I understand what you're trying to accomplish, but there's not even the hint of 
a loop in this construct. It's more or less a nested if, not a while loop. 

If you really don't like with, how about since? That's not used commonly in imperative languages and expresses 
the sense that these arguments have to be "true". 

since(  {:ok, match} <- fun1(val), 
           {:ok, match} <- fun2(val),
          do: something)

Again I really hate while, a lot.  

- Booker C Bense

P.S. did I mention that I have a strong dislike for while as a name for this construct? 

Aaron Sikes

unread,
Oct 23, 2015, 1:07:57 AM10/23/15
to elixir-l...@googlegroups.com
I think "while" was just an accident or autocorrect and he was only discussing syntax/parentheses

tal...@gmail.com

unread,
Oct 23, 2015, 1:09:12 AM10/23/15
to elixir-l...@googlegroups.com
Will it be possible to implement with in a way that is compatible with existing versions of elixir, implemented as a library? I am thinking perhaps as a macro and perhaps if you add extra parenthesis?

Chris McCord

unread,
Oct 23, 2015, 1:11:09 AM10/23/15
to elixir-l...@googlegroups.com
It’s my turn to be crazy. Let me preface with I’m 100% onboard with the original proposal and I very much favor the explicit approach. But Michael and Michał’s concerns got me thinking, and I had too much caffeine, so I present you with protocol driven, indent friendly, verbose free when desired, `with`:

`Match` is the default `with` matcher, part of std lib protocol and would support an idea of an “executor” and a “matcher"

with Match,
  %{halted: false} = conn <- plug1(conn, opts1),
  %{halted: false} = conn <- plug2(conn, opts2),
  %{halted: false} = conn <- plug3(conn, opts3),
  do: conn

Oh you think the verboseness is too much? Protocols!

with Plug,
  conn <- plug1(conn, opts1),
  conn <- plug2(conn, opts2),
  conn <- plug3(conn, opts3),
  do: conn


Oh you want to run in a task? We have a module for that!

with Task in Match,
  part1 <- fetch_part("foo"),
  part2 <- fetch_part("bar"),
  {:ok, page1} <- fetch_page(part2),
  {:ok, page2} <- fetch_page(part1),
  do: {page1, page2}


Oh you wish you could match on common ok/error patterns? Just implement your own OkMatch and ErrorMatch matchers:


with Task in OkMatch,
  part1 <- fetch_part("foo"),
  part2 <- fetch_part("bar"),
  page1 <- fetch_page(part2),
  page2 <- fetch_page(part1),
  do: {page1, page2}


¯\_(ツ)_/¯


-- 
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-co...@googlegroups.com.

Chris McCord

unread,
Oct 23, 2015, 1:43:41 AM10/23/15
to elixir-l...@googlegroups.com
s/protocol/contract for my previous brain dump

> On Oct 23, 2015, at 1:09 AM, tal...@gmail.com wrote:
>
> Will it be possible to implement with in a way that is compatible with existing versions of elixir, implemented as a library? I am thinking perhaps as a macro and perhaps if you add extra parenthesis?
>
> --
> 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-co...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/3E85005D-5373-4F8D-8AD6-D2DCB4213802%40gmail.com.

Julian Haeger

unread,
Oct 23, 2015, 4:29:21 AM10/23/15
to elixir-lang-core, jose....@plataformatec.com.br
I think there's potential in some syntax around this. I can imagine it being simpler to explain what's happening to someone new to elixir and that seems a good test to me.

On Thursday, 22 October 2015 17:40:03 UTC+1, Tallak Tveide wrote:
 Such great fun just to follow the evolution of Elixir. Like before Jose, you just interrupt the discussion of monads and pipe operator variations with a completely different, and quite obivously good suggestion. We could all have come up with that particular solution, but you were the one to do it :)


Before I start any criticism, let me just make it clear that I really like the suggested change. It seems quite similar to Haskell's let .. in construct.

Now I haven't had the time to digest this properly, but my critisism would be that the formatting is less readable compared to the pipe that we like so much. The `do` block seems a bit redundant for many uses. I think if we were to keep the piping, we only needed to have pattern filters along the pipes:

# using with

with {:ok, binary} <- File.read(path),
         {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
         do: {:ok, wrap(data)}

# using pipe patterns

File.read(path)
|> [{:ok, &}] :beam_lib.chunks(:abstract_code))
|> [{:ok, &}] wrap

I realize that this particular syntax is full of issues, just food for thought...

José Valim

unread,
Oct 23, 2015, 4:51:25 AM10/23/15
to Julian Haeger, elixir-lang-core
Thanks everyone for the feedback,

After hearing everyone's concerns and appraisal regarding "with", we have decided to introduce this feature to the language, as proposed. We believe the similarities with "for" and the shape imposed by "with" have more to add than detract compared to the other alternatives.

I am moving this proposal to the issues tracker and it will be part of Elixir 1.2.

Have fun!



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

Tymon Tobolski

unread,
Oct 23, 2015, 4:57:39 AM10/23/15
to elixir-l...@googlegroups.com, Julian Haeger
That's great, can't wait \o/

Jose - what's the roadmap for deprecation of 'with' ? I'm using this name in a similar manner to plug's 'plug' so I'll need to change that to something else 

(and btw if someone has an idea for different name please let me know ;) )
---

Monterail

Tymon Tobolski • CTO

at Monterail, Ruby development and design agency with love

skype: nomaet • twitter: @iteamon


--
You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-core/lzNhT87-XUU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2BMMnF1oHXHOk2K%2B2X8tqnc2tXtVj41MTdLwWafdzW5jQ%40mail.gmail.com.

José Valim

unread,
Oct 23, 2015, 5:08:32 AM10/23/15
to elixir-l...@googlegroups.com
Tymon, there will be no deprecation. When Elixir v1.0 released, we have pointed out that we may still have breaking changes in our releases when we add functions to modules. For example, if you "import Integer", and we add functions to Integer, it may conflict with your code. It is more common though with Kernel and special forms that are auto-imported.

It would be too much if we had to deprecate functions every time a new function is added to the standard library. Regarding the alternative to "with", maybe "through" or "thru"? It was one of the names Dave Thomas suggested to be used instead of "with".



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

--
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-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAPCQOz2mHhg%3DtuxnBHmAopkfgfzqAj9wBXsCoGH6_FmViihjeQ%40mail.gmail.com.

Michał Muskała

unread,
Oct 23, 2015, 5:33:50 AM10/23/15
to elixir-l...@googlegroups.com
After late night discussion with José and Andrea on the IRC, sleeping on the matter, and giving it some more though, I'm growing more and more in favour of the original for-like with proposal.

There was a valid point raised that the block-form with could be easily overused, as it would allow any expressions inside. This could be extremely confusing when nesting one with inside another, possibly with different modifiers. I came up with the horrible idea of defwith that would wrap the entire function's body inside a block-style with - this will make it basically an implicit, macro-powered try-catch. This idea scared me enough to seriously reconsider my proposal.

On the other hand, I have a hard time imagining how `transact with` could be implemented. Would transact be a macro? If so would it have to go and rewrite the whole with construct? This would mean basically reimplementing with for each of the modifiers. I liked the idea that Chris proposed of placing the modifier, as the first argument to with. This would allow to leverage standard behaviour or protocol powered polymorphism, instead of rewriting with with macros for each modifier. Or am I missing something and the whole idea of the modifier was for it to be a macro, and have the ability to rewrite the with?

Michał.

José Valim

unread,
Oct 23, 2015, 5:45:43 AM10/23/15
to elixir-l...@googlegroups.com
On the other hand, I have a hard time imagining how `transact with` could be implemented. Would transact be a macro?

We will have a couple functions in Macro that will receive the AST and parse it out for us. This function can be shared for those implementing either custom "with"s or custom "for"s. Similar to Macro.pipe and Macro.unpipe. Although I think the bulk of it can't really be shared.

Tymon Tobolski

unread,
Oct 23, 2015, 5:53:35 AM10/23/15
to elixir-l...@googlegroups.com
Legit, thanks a lot.

Greetings from Poland :)

Michael Schaefermeyer

unread,
Oct 23, 2015, 5:56:09 AM10/23/15
to elixir-lang-core, jose....@plataformatec.com.br
Very happy about this.

Like I stated earlier, the "syntactic" concerns seem manageable and are far outweighed by the possibilities this gives us. Thank you!

One thing to: 

The while I was using above absolutely was accidental. That's what you get for writing replies at 1:15am. I apologise.

However I do want to note that we should watch how we express ourselves here - and this critique is clearly directed at Booker. While I understand (and share) your strong opinion, hate is a pretty strong word. The way you hammered your point home reminded me of OSS communities we would strive to not become like (example: Linux Kernel). So while my mistake was stupid, please let's always keep a friendly tone. This is one of the many things that make this community so awesome: The respect and fun we have for and with one another while doing this.

Thanks

X4lldux

unread,
Oct 23, 2015, 5:57:26 AM10/23/15
to elixir-lang-core, jose....@plataformatec.com.br
What about exeptions?

What would happen if plug2 throw an exception?
    with %{halted: false} = conn <- plug1(conn, opts1),
         %{halted: false} = conn <- plug2(conn, opts2),
         %{halted: false} = conn <- plug3(conn, opts3),
         do: conn

Or what would happen if in `async` one of tasks was killed?


W dniu czwartek, 22 października 2015 03:20:32 UTC+2 użytkownik José Valim napisał:
Note: Proposal can also be read on gist. Please do not drop comments on Gist, the discussion should happen on this thread.

`with` is `for`'s younger brother. Imagine we have two functions:

    def ok(x), do: {:ok, x}
    def error(x), do: {:error, x}

While `for` is used to match on values out of a collection:

    for {:ok, x} <- [ok(1), error(2), ok(3)], do: x
    #=> [1, 3]

`with` is used to match on values directly:

    with {:ok, x} <- ok(1),
         {:ok, y} <- ok(2),
         do: {:ok, x + y}
    #=> {:ok, 3}

Because all values matched, the `do` block was executed, returning its result. If a value does not match, it will abort the chain:

    with {:ok, x} <- ok(1),
         {:ok, y} <- error(2),
         do: {:ok, x + y}
    #=> {:error, 2}

Since `error(2)` did not match `{:ok, y}`, the `with` chain aborted, returning `{:error, 2}`. 

There are many different scenarios on every day Elixir code that we can use `with`. For example, it is useful to avoid nesting "case"s:

    case File.read(path) do
      {:ok, binary} ->
        case :beam_lib.chunks(binary, :abstract_code) do
          {:ok, data} ->
            {:ok, wrap(data)}
          error ->
            error
        end
      error ->
        error
    end

Can now be rewritten as:

    with {:ok, binary} <- File.read(path),
         {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
         do: {:ok, wrap(data)}

Another example is Plug itself. Plug will only call the next plug if the `:halted` field in the connection is false. Therefore a whole plug pipeline can be written as:

    with %{halted: false} = conn <- plug1(conn, opts1),
         %{halted: false} = conn <- plug2(conn, opts2),
         %{halted: false} = conn <- plug3(conn, opts3),
         do: conn

If any of them does not match, because `:halted` is true, the pipeline is aborted.

Similarly to `for`, variables bound inside `with` won't leak. Also similar to `for`, `with` allows "bare expressions". For example, imagine you need to calculate a value before calling the next match, you may write:

    with {:ok, binary} <- File.read(path),

José Valim

unread,
Oct 23, 2015, 6:14:31 AM10/23/15
to elixir-l...@googlegroups.com
What about exeptions?

What would happen if plug2 throw an exception?
    with %{halted: false} = conn <- plug1(conn, opts1),
         %{halted: false} = conn <- plug2(conn, opts2),
         %{halted: false} = conn <- plug3(conn, opts3),
         do: conn

Exception won't be wrapped or rescue in any case i.e. it will blow up. 
 
Or what would happen if in `async` one of tasks was killed?

Same thing as would happen in tasks today. Because they are all linked, the caller and all linked tasks will crash.

José Valim

unread,
Oct 23, 2015, 6:22:24 AM10/23/15
to elixir-l...@googlegroups.com
However I do want to note that we should watch how we express ourselves here - and this critique is clearly directed at Booker. While I understand (and share) your strong opinion, hate is a pretty strong word.

Thanks for raising those concerns Michael and I agree with you.

I always think about newcomers. Both you and Booker are part of the community for a long while but imagine if your proposal was written by someone trying to contribute for the first time. Saying his proposal is "hated" could potentially turn the person off from future discussions.

In any case, I don't believe Booker had bad intentions at all, it was just a mistake. And that's the point, we all make mistakes and we all make bad proposals from time to time. :)

Michael Schaefermeyer

unread,
Oct 23, 2015, 6:50:34 AM10/23/15
to elixir-lang-core, jose....@plataformatec.com.br
José,

Thanks for weighing in.

Just to be clear: No feelings were hurt. I did not think that Booker had bad intentions at all. I actually felt sorry for addressing you, Booker, like that.
However I do want to nip these things in the bud right-away.

So: No harm no foul, just a warning to all of us to keep this as friendly, respectful and fun as it is.

Back to the awesomeness that is with!

Onorio Catenacci

unread,
Oct 23, 2015, 7:50:43 AM10/23/15
to elixir-lang-core
Of course--my bad for not being a bit more careful with my syntax.  Thanks José.

eksperimental

unread,
Oct 23, 2015, 8:23:00 AM10/23/15
to elixir-l...@googlegroups.com
Hi everyone, I have just read the whole dicussion about "with",
But I have the need to express this for consideration (even though I'm
aware nothing can be done about it)
Does anybody feel that the "<-" operator is counter-intuitive, as I do.

Elixir works with data moving it always from left (data) to right
(data element), such as in the "fn .. ->", "|>",

I feel it breaks with the elixir flow of reading code,

When I see a "for", i need to switch my reading mode (and slowdown
my flow) and just to the right hand side of "->" and then to the left,
specially when more than generators are used in the same line.

iex> for x <- [1, 2, 3, 4, 5], y <- [6, 7, 8, 9, 10], do: x*y

my eyes need to find "[1, 2, 3, 4, 5]" and go back to "x", and then
jump to "[6, 7, 8, 9, 10]" to later jump back to "y" and finally jump
forward to "do:".


When i read the "with" examples, I feel the same way,
That i need to go reading back and forth to understand what the code
is doing.

I just wanted to bring it to light, since once this is implemented,
there won't be going back.
I hope it just me, but I would be interested to know if this
happens to anybody else.
cheers


On Thu, 22 Oct 2015 03:20:10 +0200
José Valim <jose....@plataformatec.com.br> wrote:

> Note: Proposal can also be read on gist
> <https://gist.github.com/josevalim/8130b19eb62706e1ab37>. Please do
> not drop comments on Gist, the discussion should happen on this
> thread.
>
> `with` is `for`'s younger brother. Imagine we have two functions:
>
> def ok(x), do: {:ok, x}
> def error(x), do: {:error, x}
>
> While `for` is used to match on values out of a collection:
>
> for {:ok, x} <- [ok(1), error(2), ok(3)], do: x
> #=> [1, 3]
>
> `with` is used to match on values directly:
>
> with {:ok, x} <- ok(1),
> {:ok, y} <- ok(2),
> do: {:ok, x + y}
> #=> {:ok, 3}
>
> Because all values matched, the `do` block was executed, returning its
> result. If a value does not match, it will abort the chain:
>
> with {:ok, x} <- ok(1),
> {:ok, y} <- error(2),
> do: {:ok, x + y}
> #=> {:error, 2}
>
> Since `error(2)` did not match `{:ok, y}`, the `with` chain aborted,
> returning `{:error, 2}`.
>
> There are many different scenarios on every day Elixir code that we
> can use `with`. For example, it is useful to avoid nesting "case"s:
>
> case File.read(path) do
> {:ok, binary} ->
> case :beam_lib.chunks(binary, :abstract_code) do
> {:ok, data} ->
> {:ok, wrap(data)}
> error ->
> error
> end
> error ->
> error
> end
>
> Can now be rewritten as:
>
> with {:ok, binary} <- File.read(path),
> {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
> do: {:ok, wrap(data)}
>
> Another example is Plug itself. Plug will only call the next plug if
> the `:halted` field in the connection is false. Therefore a whole
> plug pipeline can be written as:
>
> with %{halted: false} = conn <- plug1(conn, opts1),
> %{halted: false} = conn <- plug2(conn, opts2),
> %{halted: false} = conn <- plug3(conn, opts3),
> do: conn
>
> If any of them does not match, because `:halted` is true, the
> pipeline is aborted.
>
> Similarly to `for`, variables bound inside `with` won't leak. Also
> similar to `for`, `with` allows "bare expressions". For example,
> imagine you need to calculate a value before calling the next match,
> you may write:
>
> with {:ok, binary} <- File.read(path),
> header = parse_header(binary),
> {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
> do: {:ok, wrap(data)}
>
> That's pretty much the gist of it. Thoughts?
>
> *## FAQ*
>
> *Q: Why `with` instead of one of the operators proposed on other
> discussions?*
>
> The operators in the other discussions were always bound to some
> particular shape. For example, it would always match on `{:ok, _}`,
> which is limitting. `with` explicitly lays out the pattern, which
> means you can match on any value you want.
>
> *Q: Will `with` work with the pipe operator?*
>
> The answer is no. with, for, the pipe operator are implementations of
> different monads and we know from other comunities monads do not
> compose well. Even without a type system, the only way to introduce
> `with` that works with pipe is by defining something like `withpipe`,
> and maybe `withfor`, which obviously wouldn't scale because there are
> too many combinations. Furthermore, we really appreciate that `with`
> makes all the patterns explicit, instead of hiding it behind a pipe
> operator. `with` also gives us full control on how the arguments are
> forwarded.
>
> *## José goes crazy*
> *José Valim*

Booker Bense

unread,
Oct 23, 2015, 8:42:43 AM10/23/15
to elixir-l...@googlegroups.com
My sincerest apologies, I was trying to be amusing and it fell flatter than a pancake. 

- Booker C. Bense 
--
You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-core/lzNhT87-XUU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elixir-lang-co...@googlegroups.com.

Michael Schaefermeyer

unread,
Oct 23, 2015, 8:50:22 AM10/23/15
to elixir-lang-core
Booker,

ah - the good old misunderstanding as a result of (lack of) media richness. In that case I apologise for proving to be quite german when it comes to understanding irony and humour.

We are more than good, looking forward to continuing the discussion!

Michael

Am Freitag, 23. Oktober 2015 14:42:43 UTC+2 schrieb Booker Bense:
My sincerest apologies, I was trying to be amusing and it fell flatter than a pancake. 
- Booker C. Bense 

On Friday, October 23, 2015, Michael Schaefermeyer <michael.sc...@gmail.com> wrote:
Very happy about this.

Like I stated earlier, the "syntactic" concerns seem manageable and are far outweighed by the possibilities this gives us. Thank you!

One thing to: 

The while I was using above absolutely was accidental. That's what you get for writing replies at 1:15am. I apologise.

However I do want to note that we should watch how we express ourselves here - and this critique is clearly directed at Booker. While I understand (and share) your strong opinion, hate is a pretty strong word. The way you hammered your point home reminded me of OSS communities we would strive to not become like (example: Linux Kernel). So while my mistake was stupid, please let's always keep a friendly tone. This is one of the many things that make this community so awesome: The respect and fun we have for and with one another while doing this.

Thanks

Am Freitag, 23. Oktober 2015 11:45:43 UTC+2 schrieb José Valim:
On the other hand, I have a hard time imagining how `transact with` could be implemented. Would transact be a macro?

We will have a couple functions in Macro that will receive the AST and parse it out for us. This function can be shared for those implementing either custom "with"s or custom "for"s. Similar to Macro.pipe and Macro.unpipe. Although I think the bulk of it can't really be shared.

--
You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-core/lzNhT87-XUU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elixir-lang-core+unsubscribe@googlegroups.com.

Ed W

unread,
Oct 23, 2015, 1:28:13 PM10/23/15
to elixir-l...@googlegroups.com

Nami Doc

unread,
Nov 7, 2015, 4:42:43 AM11/7/15
to elixir-lang-core, jose....@plataformatec.com.br
Looks like a pretty great proposal... 

The crazy part is amazing as well. That means we get "user-defined" `with`s? 

As soon as the chain is broken, the pattern that broke it is returned, correct?

Le jeudi 22 octobre 2015 03:20:32 UTC+2, José Valim a écrit :
Note: Proposal can also be read on gist. Please do not drop comments on Gist, the discussion should happen on this thread.
    with %{halted: false} = conn <- plug1(conn, opts1),
         %{halted: false} = conn <- plug2(conn, opts2),
         %{halted: false} = conn <- plug3(conn, opts3),
         do: conn

If any of them does not match, because `:halted` is true, the pipeline is aborted.

Similarly to `for`, variables bound inside `with` won't leak. Also similar to `for`, `with` allows "bare expressions". For example, imagine you need to calculate a value before calling the next match, you may write:

    with {:ok, binary} <- File.read(path),
         header = parse_header(binary),
         {:ok, data} <- :beam_lib.chunks(header, :abstract_code),
         do: {:ok, wrap(data)}

That's pretty much the gist of it. Thoughts?

## FAQ

Q: Why `with` instead of one of the operators proposed on other discussions?

The operators in the other discussions were always bound to some particular shape. For example, it would always match on `{:ok, _}`, which is limitting. `with` explicitly lays out the pattern, which means you can match on any value you want.

Q: Will `with` work with the pipe operator?

The answer is no. with, for, the pipe operator are implementations of different monads and we know from other comunities monads do not compose well. Even without a type system, the only way to introduce `with` that works with pipe is by defining something like `withpipe`, and maybe `withfor`, which obviously wouldn't scale because there are too many combinations. Furthermore, we really appreciate that `with` makes all the patterns explicit, instead of hiding it behind a pipe operator. `with` also gives us full control on how the arguments are forwarded.

## José goes crazy

To mimic my ElixirConf keynotes, where the talk starts with reasonable proposals and ends-up with me babbling stuff that may not ever see the day of light, let's do it a bit in written form too. :)

In 2014, I had proposed `stream for` and `parallel for` support alongside `for` comprehensions. Could we have similar modifiers for `with` too? I am glad you asked!

We could at least introduce `async with`:

    async with part1 <- fetch_part("foo"), # async


José Valim

Gabriel Jaldon

unread,
Nov 7, 2015, 5:20:30 AM11/7/15
to elixir-l...@googlegroups.com

That's correct, Nami. This is going to be implemented in an upcoming version of Elixir. 1.2, I think.

--
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-co...@googlegroups.com.

James Fish

unread,
Nov 7, 2015, 11:59:55 AM11/7/15
to elixir-l...@googlegroups.com
Do the "bare expressions" in a `with` work the same as a `for` so that `with` only continues if the "bare expression" returns a truthy? If a "bare expression" returns a falsey does the last `result <-` get returned?

Providing `for`'s truthy/falsey filtering expressions would allow guard-like semantics that can't be expressed by matching alone.

José Valim

unread,
Nov 7, 2015, 2:49:24 PM11/7/15
to elixir-l...@googlegroups.com
That's a very good point James. The big question is: what to return when the "bare expression" evaluates to false?



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

Theron Boerner

unread,
Nov 7, 2015, 2:52:56 PM11/7/15
to elixir-l...@googlegroups.com
I'm not a fan of putting the majority of the code before the `do`. Why not something like this?

with do 
   %{halted: false} = conn <- plug1(conn, opts1)
   %{halted: false} = conn <- plug2(conn, opts2)
   %{halted: false} = conn <- plug3(conn, opts3)
   conn
end

José Valim

unread,
Nov 7, 2015, 3:05:52 PM11/7/15
to elixir-l...@googlegroups.com
Theron, such syntax was already proposed and discussed before. You can review the discussion if you are interested but at some point I mentioned:

> We believe the similarities with "for" and the shape imposed by "with" have more to add than detract compared to the other alternatives.

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

Theron Boerner

unread,
Nov 7, 2015, 3:12:44 PM11/7/15
to elixir-l...@googlegroups.com
Right, that makes sense if it's related to for.

Peter Saxton

unread,
Nov 8, 2015, 10:09:34 AM11/8/15
to elixir-lang-core, jose....@plataformatec.com.br
So thinking some more on the problem that this proposal is aiming to solve I have some comments.

As I understand it match errors for error cases is the main issue.
The value of ok/error tuples is that functions can be pure rather than raising an exception.

Using the example in the linked proposal, which is.

with {:ok, x} <- ok(1),
     {:ok, y} <- error(2),
     do: {:ok, x + y}
#=> {:error, 2}

Would there be value in adding the following type of structure
def myfunc do
  {:ok, x} = ok(1)
  {:ok, y} = error(2)
  {:ok, x + y}
mismatch {:error, _} = e
  e
end

This looks very similar handling errors in for example ruby.

The key difference is that this function can be pure and pattern matching can be used on the resulting function.

This separates the error handling from the bind(<-) operator. I felt the bind operator was limited when you had to declare the match for it separately.

James Fish

unread,
Nov 8, 2015, 11:13:01 AM11/8/15
to elixir-l...@googlegroups.com
I am not suggesting to use this but a more powerful version of that pattern already exists:

def myfunc do
  {:ok, x} = ok(1)
  {:ok, y} = error(2)
  {:ok, x+y}
catch
  :error, {:badmatch, {:error, _} = error} ->
    error
end

Of course this function would catch match errors from ok/1 and error/2 and not just those occurring in myfunc/0.

try do.. else.. provides a similar pattern to you describe though:

try do
  myfunc()
else
  {:ok, n}   -> n
  {:error, 1} -> raise "error 1"
  {:error, 2} -> raise "error 2"
end

Your issue with `with` could be solved if the same `else` clause was added to the current proposal:


with {:ok, x} <- ok(1),
       {:ok, y} <- error(2),
do
  x+y
else
  {:error, 1} -> raise "error 1"
  {:error, 2} -> raise "error 2"
end

If the `else` clause is not present in `try` the result is returned as the following clause existed:

else: (result -> result)

The same would then be true for `with` and would match the current proposal.

--
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-co...@googlegroups.com.

James Fish

unread,
Nov 8, 2015, 11:18:06 AM11/8/15
to elixir-l...@googlegroups.com
I think it would have to be as in this example:

{:ok, 1} = with {:ok, n} <- ok(1), n == 2, do: {;ok, 2}

That is the result is always the result of the last bind (<-) if the `with` does not reach `do`



Toby Hede

unread,
Nov 14, 2015, 6:23:13 AM11/14/15
to elixir-lang-core, jose....@plataformatec.com.br
<3 this idea.

Is there an implementation available for experimentation or is this something that has to be baked in to a release?

José Valim

unread,
Nov 14, 2015, 7:57:08 AM11/14/15
to Toby Hede, elixir-lang-core
The implementation is already in master thanks to lexmag. :)
--

Vincent Siliakus

unread,
Nov 19, 2015, 4:06:00 AM11/19/15
to elixir-lang-core, jose....@plataformatec.com.br
Op zaterdag 14 november 2015 13:57:08 UTC+1 schreef José Valim:
The implementation is already in master thanks to lexmag. :)


Works great, I love this addition!

I was wondering though since the current implementation uses 'case' internally, if support for something like this is still planned:

with x when x < 2 <- 4, do: :ok
=> 4 

with x when x > 2 <- 4, do: :ok
=> :ok

I think this would allow even more clunky, nested code to be rewritten much more elegantly with 'with'.

José Valim

unread,
Nov 19, 2015, 10:54:15 AM11/19/15
to Vincent Siliakus, elixir-lang-core
Please open up an issue. If we support such, we would need to support it both on "comprehensions" and "with".



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

Fernando Tapia Rico

unread,
Sep 8, 2017, 8:07:07 AM9/8/17
to elixir-lang-core
I wanted to share with you guys https://github.com/fertapric/async_with, which is the implementation of "async with", one of the "José goes crazy" ideas mentioned in this proposal :) I hope you like it!

Any feedback or help is more than welcome!

Theron Boerner

unread,
Sep 8, 2017, 3:52:00 PM9/8/17
to elixir-l...@googlegroups.com
Hi Fernando,

Great work on this implementation! However, there are a few edge cases that you do not handle. E.g.

async with a <- 1,
  b <- :timer.sleep(1000),
  c <- IO.inspect(a),
  d <- {a, b} do
...
end

That should print out "1" immediately because c only has a dependency on a and is unrelated to b, however, your implementation does not. I've been discussing some of these edge cases with José, Luke Imhoff, et al for a few months and slowly built up this gist. As you can see, that gist is a complete mess so Luke and I began to rewrite it in https://github.com/kronicdeth/async_with. John Wahba proposed an alternative implementation to me at ElixirConf that eliminates the need to compute a dependency graph so we're going to be implementing that version sometime soon and share it here when we have a working version on GitHub.

- Theron

--
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-co...@googlegroups.com.

Fernando Tapia Rico

unread,
Sep 8, 2017, 5:12:47 PM9/8/17
to elixir-lang-core
Thanks for the feedback Theron!

Interesting...instead of using the dependency graph you are spawning new processes when all the conditions (the right vars) are matched. It's definitely an idea worth exploring.

John Wahba

unread,
Sep 9, 2017, 5:16:19 PM9/9/17
to elixir-lang-core
I took a bit of a different approach: instead of spawning new processes when all the conditiones match: I spawn them all as futures and they block until their dependent futures return:
https://github.com/johnwahba/async_with

Fernando Tapia Rico

unread,
Sep 10, 2017, 9:43:56 AM9/10/17
to elixir-lang-core
I just published async_with v0.2.0 that ditches the dependency graph and uses processes + messages to execute the clauses asynchronously. https://github.com/fertapric/async_with

I've also included a new test with the edge case you mentioned https://github.com/fertapric/async_with/blob/master/test/async_with_test.exs#L534-L554

Here is the PR with all the changes: https://github.com/fertapric/async_with/pull/1

I would like to mention you guys in the README for all the help. Could give me a list of name + handlers that I should include?

Thanks!

Theron Boerner

unread,
Aug 16, 2018, 5:59:32 PM8/16/18
to elixir-lang-core
Fernando,

Sorry for just now responding, I don't know what happened to this email. /shrug.

If you want to give me credit, @hunterboerner is my GitHub handle and my name is Theron Boerner. Need that résumé material. :)

— Theron

John Wahba

unread,
Aug 18, 2018, 2:59:49 AM8/18/18
to elixir-lang-core
@johnwahba on GitHub, thanks for continuing to maintain it :)
Reply all
Reply to author
Forward
0 new messages