2

I would like to create an http client in elm for external api. In scala, which is OO/FP mix, I would express this (forgetting about async for now) simply as:

class Client(url: String) {
    def getFoo(): String = ???
}

but in Elm Im a bit lost. The obvious solution is to pass url directly to the function

module Client

getFoo : String -> String

but this is extremely painful to use because it complicates every call, so the burden grows with both the number of functions defined and the number of calls to these functions.

I tried to use records with functions, like:

type alias Client = { getFoo: String }

createClient : String -> Client

but it feels like a poor imitation of OOP. AFAIU this is solved by Functors in Ocaml and objects in OOP.

What is a canonical way to do this in Elm (or Haskell if Elm is lacking some particular feature here)?

Krever
  • 1,371
  • 1
  • 13
  • 32
  • If I understand this question correctly, it's about how to do Dependency Injection in FP. If so, you may find my article series on the topic relevant: [From dependency injection to dependency rejection](http://blog.ploeh.dk/2017/01/27/from-dependency-injection-to-dependency-rejection). In summary, in Haskell one would either use free monads, or alternatively type classes. – Mark Seemann Oct 27 '18 at 10:27
  • 1
    Perhaps this F# question, and its answers, could be illuminating as well: https://stackoverflow.com/q/34011895/126014 – Mark Seemann Oct 27 '18 at 10:28

1 Answers1

5

Remeber that OO method calling is nothing but syntactic sugar for supplying an extra this / self argument to the function:

--  OO                       ┃      functional/procedural
Client c = ...;              │     c = ... :: Client
...                          │     ...
main() {print(c.getFoo());}  │     main = print(getFoo c)

It is thus quite possible, and often useful, to go this route, both in a procedural language like C and in an FP language.

data Client {
    url :: String
  , ...
  }

getFoo :: Client -> String
getFoo (Client{url = u}) = ...

Yes, that requires you to explicitly pass the Client object around, but this isn't necessarily a bad thing – provided you have properly distinguished types, it can be pretty obvious what needs to be passed as which argument of what function, and this approach actually scales better than OO methods because you can have multiple objects as arguments, and each function can take just those that it needs.

Of course, there are situation where you do have a whole bunch of functions that all need the same object, and would like to have it happen under the hood without explicitly passing it everywhere. That can be done by hiding it in the result type.

type Reader c r = c -> r

getFoo :: Reader Client String
getBar :: Reader Client Int
getBaz :: Reader Client Double

This reader monad can be used with standard monad combinators:

 quun = (`runReader`c) $ do
   foo <- getFoo     -- `c` argument implicitly passed
   bar <- getBar
   baz <- getBaz
   return (calcQuun foo bar (2*baz))

This approach is particularly useful if you also have mutation in your methods, as is commonplace in OO. With explicit passing, this becomes very cumbersome indeed as you need to work with updated copies and need to be careful to pass the correct version to each function. With the state monad, this is handled automatically as if it were true mutation.


I disregard inheritance here. If you call a method through a superclass pointer, there's an extra vtable lookup, but that can be modelled as just another field in the record type that tells you what subclass this object belongs to.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • Sadly Reader monad comes with the standard problem of monads -> they don't compose. So I either have to keep all my code in the context of `Reader Client` or use things like `ReaderT`. In either case, I'm limited in comparison to OOP. But thanks for a detailed answer anyway, you confirmed my understanding of the topic :) – Krever Oct 27 '18 at 10:22
  • @Krever `ReaderT` is how monads **do** compose. This isn't limited in comparison to OO (I like to say: OO languages are more limited since they have only exactly one monad, `StateT This IO`, whereas Haskell and Elm allow you to choose the appropriate monad for every situation). – leftaroundabout Oct 27 '18 at 10:33
  • Constraints liberate and liberties constraints. Being constrained to exactly one monad gives a lot of liberty when writing code, e.g. by not having to care about proper transformer stack. Nevertheless, I think I see the point now and passing a record to function is not really different from calling function on an object - the number of moving parts is the same. – Krever Oct 27 '18 at 10:38
  • found this – [`Reader.elm`](https://gist.github.com/jliuhtonen/189facfd3841e0c1888b58caf4bfb9aa) – Mulan Oct 27 '18 at 16:18