Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the conventional way to use `IHttpClientFactory` in F#?

So in C# .NET Core apps I can use all the fancy DI stuff to cache HttpClient so that I avoid socket exhaustion, optimizing connecting to server and so on:

services.AddHttpClient<IRandomService, RandomService>();

And now in F#... I just have a simple function in a console app that checks the validity of an url and loops against different urls:

let isReachable (url: string) = 
    (new HttpClient()).GetAsync(url) 
    |> Async.AwaitTask 
    |> Async.RunSynchronously
    |> fun response -> response.IsSuccessStatusCode

I know I can bring here interfaces and state and all the OOP but gurus don't encourage such ideas so I just wonder if there is some totally different super functional approach for that.

like image 630
psfinaki Avatar asked Nov 01 '25 09:11

psfinaki


1 Answers

The canonical answer: Generally if you want to reuse/cache something, you would pass that as a parameter to your function:

let isReachable' (client: HttpClient) url = 
    client.GetAsync(url) |> ...

Note the tick after the function name. This is on purpose, because then I will partially apply it like this:

let oneTrueHttpClient = new HttpClient()
let isReachable = isReachable' oneTrueHttpClient

thus getting back my original isReachable function with one parameter, but now all calls are using the same instance of HttpClient.

This partial application would be done at program startup, and then the isReachable function would be passed as parameter to anybody who needs it.

Of course, you might want to have a pool of HTTP clients instead, one for each thread or something like that. In that case, you would use a factory function instead of the instance itself:

let isReachable' (getClient: unit -> HttpClient) url = 
    getClient().GetAsync(url) |> ...

// And later:
let clientFactory () = magic.MakeClient()  // <- insert caching behavior here
let isReachable = isReachable' clientFactory

The practical answer: But of course, the above doesn't scale well. For example, now whoever needs to use isReachable can't just straight up call it out of the static context, they have to get it as a parameter from whoever is calling them, and so on and so on. This doesn't mean that you have to tunnel through absolutely everything everywhere, though. You can partially apply everything that can be partially applied at program initialization. This would be equivalent to manually implementing an IoC container, except instead of specifying container.Add<IHttpClient, HttpClient> or whatever, you're passing functions around.

This is doable. I have done it. But it does get kinda ugly over time.

So the practical answer is: religion is for spiritual growth, for everything else use common sense.

It's totally ok to stray away from The Holy Functional Principles and go use an IoC container, or interfaces, or whatever. After all, interfaces are the only mechanism F# has for achieving higher-rank behavior, so why not?

This is especially true because most "frameworks" like WPF, ASP.NET, etc. are inherently object-oriented, so you'd be jumping through hoops trying to avoid that for no good reason.


The general rule that I personally have adopted is this: express your actual business logic in functional style, as clearly and purely as you can, but at the boundaries with the outside world (i.e. OS or whatever framework you're using) feel free to do whatever the outside world expects you to do.

Applying to your particular example, the isReachable function very clearly belongs to the boundary with the outside world. So feel free to implement it in whatever way fits.

like image 71
Fyodor Soikin Avatar answered Nov 04 '25 01:11

Fyodor Soikin