top | item 45725779

(no title)

POiNTx | 4 months ago

In Elixir this would be written as:

  db.getUsers()
  |> getExpiredUsers(Date.now())
  |> generateExpiryEmails()
  |> email.bulkSend()
I think Elixir hits the nail on the head when it comes to finding the right balance between functional and imperative style code.

discuss

order

time4tea|4 months ago

Not a single person in this thread commented on the use of Date.now() and similar - surely clock.now() - you never ever want to use global time in any code, how could you test it?

clock in this case is a thing that was supplied to the class or function. It could just be a function: () -> Instant.

(Setting a global mock clock is too evil, so don't suggest that!)

POiNTx|4 months ago

I was just referring to how pipes make these kinds of chained function calls more readable. But on your point, I think using Date.now() is perfectly ok.

MarkMarine|4 months ago

nit picking on the example code while missing the example the code was trying to demonstrate. I see why TAOCP used pseudocode

montebicyclelo|4 months ago

    bulk_send(
        generate_expiry_email(user) 
        for user in db.getUsers() 
        if is_expired(user, date.now())
    )
(...Just another flavour of syntax to look at)

whichdan|4 months ago

The nice thing with the Elixir example is that you can easily `tap()` to inspect how the data looks at any point in the pipeline. You can also easily insert steps into the pipeline, or reuse pipeline steps. And due to the way modules are usually organized, it would more realistically read like this, if we were in a BulkEmails module:

  Users.all()
  |> Enum.filter(&Users.is_expired?(&1, Date.utc_today()))
  |> Enum.map(&generate_expiry_email/1)
  |> tap(&IO.inspect(label: "Expiry Email"))
  |> Enum.reject(&is_nil/1)
  |> bulk_send()
The nice thing here is that we can easily log to the console, and also filter out nil expiry emails. In production code, `generate_expiry_email/1` would likely return a Result (a tuple of `{:ok, email}` or `{:error, reason}`), so we could complicate this a bit further and collect the errors to send to a logger, or to update some flag in the db.

It just becomes so easy to incrementally add functionality here.

---

Quick syntax reference for anyone reading:

- Pipelines apply the previous result as the first argument of the next function

- The `/1` after a function name indicates the arity, since Elixir supports multiple dispatch

- `&fun/1` expands to `fn arg -> fun(arg) end`

- `&fun(&1, "something")` expands to `fn arg -> fun(arg, "something") end`

Akronymus|4 months ago

Not sure I like how the binding works for user in this example, but tbh, I don't really have any better idea.

Writing custom monad syntax is definitely quite a nice benefit of functional languages IMO.