Is it possible to have lazy evaluation on list comprehensions?

Against my better judgement, I’m gonna have a few remarks.

  • for comprehension, in scala is basically syntactic sugar for chains of foreach/map/flatMap/withFilter, applied to whatever object that’s on the right of the arrow. If you’re trying to learn, you must desugar those calls.
  • Scala is absolutely not lazy by default.
  • Scala is impure. There is no IO monad in the standard library. Given the fact that Martin Odersky (creator of scala), has stated many times that he doesn’t believe monads are the right model for effects, an IO monad in the standard library won’t happen.
  • I guess the IO type you’ve shown comes from the cats-effect library. Its goal is to help with concurrency, not effects. (basically, it’s really close to what lwt would offer in ocaml, in both case you get few guarantees that side-effects will be push outside)

All that being said, it’s hard to say, what your goal is. And since you brought up being pedantic,
{:ok, result} | {:error, details} does not a monad make, you still need return and bind (or in cats scala speak, pure and flatMap).

If learning fp is your goal, I’d recommend starting with Stanford CS 3110, then if you want to know more about lazy by default and pure, check out Haskell from first principles. Then if you want to understand a bit more about category theory in programming watch/read Bartosz Milewski’s work ( eg : videos on ct , or Blog posts, and Fong-Spivak-Milewski), after that you’ll pretty much end up in the realm of research papers.

If you want to apply any of the above to elixir/erlang, you’ll need to have a deep understanding of that + the primitives available in erlang/elixir.

Finally, when you use abstractions, you should consider what they actually bring to the table (and, push side effects outside doesn’t really work).

4 Likes

Honestly, I don’t feel this is great code in any language.

I confess that I don’t fully understand what it’s doing, but it looks to be primarily about finding meetings everyone could attend. I wouldn’t want code that does that related to IO operations if I could help it.

Are we talking about the list comprehension inside the functions you posted or a hypothetical list comprehension operating on the returned result of the posted functions?

I remain mostly convinced that a Stream is the Elixir answer to this question. Streams are lazy, can represent infinite data structures like a generated list of next possible meetings, and could generate no results when needed.

Gary Bernhardt’s Functional Core, Imperative Shell video shows off what I believe to be a better and more idiomatic Elixir design for the above goals.

It’s possible I just don’t understand the code or questions yet though!

2 Likes

Can’t you do this?

program = fn -> 1 end
program2 = fn -> 2 end

program3 = fn -> program.() + program2.() end

the specs won’t be what you’re looking for though.

I think I’m finally grasping what you might be trying to do. I have to say the code looks a little like gobbledygook. What is supposed to be happening? What happens to the variable existingMeetings? where does the meeting variable come from? Why does scheduleMeetings() seem to produce a collection of existingMeetings? And let’s ignore the non-idiomatic use of camelCase :slightly_smiling_face:. Anyhow, in Elixir how about one of these

@spec schedule([String.t()], integer()) :: [function]
def schedule(attendees, lengthHours) do
  for existingMeetings <- scheduledMeetings(attendees),
    possibleMeeting <- possibleMeetings(scheduledMeetings, 8, 16, lengthHours),
    is_struct(possibleMeeting, Meeting), do: fn -> createMeeting(attendees, possibleMeeting) end
end

program = schedule(attendees, lengthHours)
Enum.map(program, &(&1.()))

or

@spec schedule([String.t()], integer()) :: Stream.t()
def schedule(attendees, lengthHours) do
  for existingMeetings <- scheduledMeetings(attendees),
    possibleMeeting <- possibleMeetings(scheduledMeetings, 8, 16, lengthHours),
    is_struct(possibleMeeting, Meeting) do
      {attendees, possibleMeeting}
  end
  |> Stream.map(fn {attendees, possibleMeeting} -> createMeeting(attendees, possibleMeeting) end)
end

program = schedule(attendees, lengthHours)
Enum.to_list(program)

I’m firmly in the “it’s possible but not particularly idiomatic” camp.

For instance, here’s my closest translation of this simple IO monad from Scala:

defmodule Nomads do
  defmacro apply(a) do
    quote do
      fn -> unquote(a) end
    end
  end
  def fail(a), do: fn -> raise a end

  def map(m, f) do
    fn -> f.(m.()) end
  end

  def flat_map(m, f) do
    fn -> f.(m.()).() end
  end

  defmacro seq(args) do
    after_block =
      Keyword.get(args, :after)
      |> wrap_fun([])

    Keyword.get(args, :do)
    |> then(fn {:__block__, _, body} -> body end)
    |> Enum.reverse()
    |> Enum.reduce(after_block, fn {:<-, meta, [v, body]}, acc ->
      {
        {:., [], [{:__aliases__, [alias: false], [:Nomads]}, :flat_map]},
        meta,
        [
          wrap_fun(body, []),
          wrap_fun(acc, [v])
        ]
      }
    end)
  end

  defp wrap_fun(body, vars) do
    {:fn, [], [{:->, [], [vars, body]}]}
  end
end

This can be used like:

iex(31)> Nomads.seq do
...(31)>   v1 <- IO.gets("Hey")
...(31)>   v2 <- IO.gets("Wazzup")
...(31)> after
...(31)>   {v1, v2}
...(31)> end

This returns a zero-arity #Function which can be invoked to actually run the computation.

3 Likes

I think the key word was enforceable. You can have type specs to help with code organization but you can’t rely on the compiler to help you enforce those specs.

Wow, so many comments and so much goodness ! Thank you all for participating !

I am grateful for your remarks. I am glad you went against your better judgement. I am here to have an honest discussion, sometimes that means I will be wrong, sometimes that means I will hear things I don’t like to. Sometimes we will just agree to disagree. The important thing is to do things respectfully, which you are.

Now, I am fairly aware of how Monads work. I understand that beneath the hood its a bunch of flatMaps and Maps.

As for books, I am reading something (that I consider) more hands on approach:

I did learn and tried Ocaml a few years ago, but I found these concepts easier to grasp in the books I mentioned.
I also follow Bartosz Milewski, the guy is a legend to me. I just didn’t have the proper time to go through his entire catalog yet (it is very long).

Also, yes, I am absolutely using the cats library. Good catch, I should have mentioned it.

Overall, I am thankful for your input. It truly gives me the impression I am moving in the right direction. I just need the community’s help to tune things into something more “Elixirish”.

Well, this code is supposed to be the function you call just before you “run” the code. As in, this is supposed to be the outer layer of you application:

program = schedule(....)
program.run()

(or something like this).

Because it is the outer most layer, it has to deal with things like IO requests that are fliquery and unreliable. This piece of code tries to make it clear by having an IO type in the specs.

For me this is important because I want to know in my code which functions I can trust, and which ones can blow in my face causing horrible pain at 4 AM.

Right now, we can have an Option type and Either type Monads using Elixir’s list comprehensions. The code I show goes a little bit into “imagination land” as I also add an IO Monad and simply say “it just works, you can totally trust me”.

So for now, I want to focus on the list comprehensions inside of my functions.

I am not saying Streams are a poor solution. However, it is my current understanding that I cannot have a function return a Stream(integer) because Stream’s take anything and return anything:

If you don’t care what a Stream does, this is perfectly fine. But in my case I want dialyzer to be of some help, so I need to let dialyzer know what thing a Stream has.

I checked all of his videos and even did a detour using his architecture model some time ago. I really really liked it, but at the end the one thing that didn’t work for me was when I needed to go the shell and back from it several times in a function. It made code quite convoluted. I also based myself in this book:

Which I believe makes a good evaluation of the architecture.

In the end, I was unable to find a solution to my problems using that architecture (a function that goes back and forth). It may be I have miss learned or not fully grasped some of it’s concepts, I’ll admit. So I am turning myself into other solutions that promise “a fix”.

My aim here is to have functions with a sound type system and datastructures that make it mathematically possible to know for a fact my code will work. By work I mean “the ability to avoid a result that was not predicted and therefore produces an unexpected output”.

You can think of it as a tool to achieve and implement Paranoic Telemetry while having dialyzer help you and having code mathematically provable (or as close to it as you can get).

To this effect, I believe Monads are a good possible solution. I believe (for now) that marking functions as impure (with an IO type) makes that easier for my fleshy human brain.

I hope this long explanation somewhat makes sense :smiley:

For me, this solution has 1 issue: it is not easily composable, meaning I have to manually envelop everything in functions.
The other issue (specs) you got on your own :smiley:

With this in mind, this solution would work. But I doubt anyone would like to read it. Thus, I am inquiring IO, as a datastructure that does all of this dirty work under the hood for me. At least this is the idea.

All good questions.

  • What is supposed to be happening? : Given a list of attendees to a meeting and a duration for said meeting, find a common time slot in their calendars for the meeting.
  • What happens to the variable existingMeetings? + where does the meeting variable come from?: Not gonna lie, you found a typo/bug in the book’s code. I did not see this coming. Fixed code:

(in scala)

def schedule(attendees: List[String], lengthHours: Int): IO[Option[MeetingTime]] = {
  for {
    existingMeetings <- scheduledMeetings(attendees) # gets a list of all meetings for both attendees
    possibleMeeting = possibleMeetings(existingMeetings, 8, 16, lengthHours).headOption
    _ <- possibleMeeting match {
        case Some(meeting) => createMeeting(attendees, possibleMeeting)
        case None => IO.unit
      }
  } yield possibleMeeting
}
  • Apologies for the use of camelCase :stuck_out_tongue:

To give some more context:

  • scheduledMeetings does some IO. This IO should be lazilly evaluated as well, meaning, it should not run until the main funciton schedule runs.
  • possibleMeetings is a pure function.

The challenge here:

  • not execute scheduledMeetings until schedule runs
  • possibleMeetings must understand something that has not yet happened and work with it
  • schedule must return something that has not yet happened and typespecs have to understand if it will fail or not.

It is my understanding, this is not possible with Elixir, mainly because of the interaction between scheduledMeetings and possibleMeetings. It would be like Stream(integer) + integer, if Stream could be typed.

Overall, your solutions are quite genius and your attention to detail impeccable. I fear however, this still proves the point I concluded earlier, that lazy evaluation in Elixir with a type to represent it (be it IO or Stream) is not possible with list comprehensions (only using Macros that transform everything into a function beneath).

@al2o3cr The main idea of the discussion is to use list comprehensions. I understand this is possible using Macros like you did. Although this is truly a testament to how expandable and remarkable Elixir can be, I also agree with you this is not idiomatic. I also do not see how it dialyzer would be happy :S

Perhaps not with success typing. But I am a firm believer that Gradual typing will still have its day :smiley:

1 Like

Hi @Fl4m3Ph03n1x, this might be not related to the topic practically but I just really want to let you know.
I’ve recently been studying several functional programming concepts and I’m very impressed, which I guess so were you. I have so many questions because I want to figure it out how can make code better agains as many cases as possible, so I’ve been exploring just like you. And when I search my questions in this forum, to be honest, I can always find your posts, which it gives me a lot of help!

Just want to let you know that there are some guys like me :smile:
And thanks for all you geniuses in forum giving insight me!

2 Likes