Aff and the correct way to do async/await pattern functionally. #1111
-
Hi! I'm looking to learn more about this lib and functional programming in general. Either/Option etc has been great so far, but now my next project has mostly async code which I tried for a bit with regular Eithers but it didn't look quite correct. Then I stumbled upon Aff in the wiki, but needless to say I just got more confused... I don't quite get what they do other than the vague notion that they're for asynchronous effects. Say you have a simple api integration with a third party, you have a How would Aff change how you solve this? Because the method is Async, it has to return a Task as far as I understand, but a Task doesn't fit into the same method chains that an |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 4 replies
-
Because I don't know the library you're using, I've created this mock of it: namespace SpotifyAPI
{
public record Track(string Artist, string Title);
public record Playlist(string Name, Track[] Tracks);
public static class SpotifyHttpClient
{
public static Task<Playlist[]> GetUserPlaylistsAsync(string userId) =>
default;
}
} For the simplest version of namespace SpotifyAff
{
public static class Spotify
{
public static Aff<Seq<Playlist>> getUserPlaylist(string userId) =>
Aff(async () =>
Seq(await SpotifyAPI.SpotifyHttpClient
.GetUserPlaylistsAsync(userId))
.Map(p => new Playlist(p.Name, Seq(p.Tracks))));
}
public record Playlist(string Name, Seq<SpotifyAPI.Track> Tracks);
} Essentially we're wrapping up the Once you have wrapped up (lifted) your functionality into the So, you could filter: // Finds playlists containing songs by Led Zeppelin
Aff<Seq<Playlist>> r = Spotify.getUserPlaylist("123")
.Filter(lists =>
lists.Exists(list =>
list.Tracks.Exists(track =>
track.Artist == "Led Zeppelin"))); You could reduce and just get a sequence of numbered tracks: // Numbers each track and formats to a string
Aff<Seq<string>> r = Spotify.getUserPlaylist("123")
.Map(lists => lists.Bind(list => list.Tracks))
.Map(tracks => Naturals.Zip(tracks)
.Map(track => $"{track.Item1}. {track.Item2.Title}")
.ToSeq()); But, critically, as with all monadic types, they work with LINQ. And LINQ is how the monadic binding works (think chaining of operations). Aff<Seq<Playlist>> both = from p1 in Spotify.getUserPlaylist("123")
from p2 in Spotify.getUserPlaylist("456")
select p1 + p2; Now, because Fin<Seq<Playlist>> result = await both.Run(); Which returns a concrete result. Another thing to note is that you can call Again, I don't know the Spotify client, but I suspect that If I change the namespace SpotifyAPI
{
public class SpotifyHttpClient
{
public Task<Playlist[]> GetUserPlaylistsAsync(string userId) =>
default;
}
} Then create a 'trait' interface for all things spotify: public interface HasSpotify
{
SpotifyAPI.SpotifyHttpClient SpotifyClient { get; }
} Then I can create a runtime that is the environmental context that gets passed through the public readonly struct Runtime :
HasSpotify,
HasCancel<Runtime>
{
public Runtime(
SpotifyHttpClient spotifyClient,
CancellationTokenSource source,
CancellationToken token
)
{
SpotifyClient = spotifyClient;
CancellationTokenSource = source;
CancellationToken = token;
}
public SpotifyHttpClient SpotifyClient { get; }
public Runtime LocalCancel
{
get
{
var src = new CancellationTokenSource();
return new(SpotifyClient, src, src.Token);
}
}
public CancellationToken CancellationToken { get; }
public CancellationTokenSource CancellationTokenSource { get; }
} It also needs to implement Now we can update the public static class Spotify<RT>
where RT : struct, HasSpotify, HasCancel<RT>
{
public static Aff<RT, Seq<Playlist>> getUserPlaylist(string userId) =>
Aff<RT, Seq<Playlist>>(async rt =>
Seq(await rt.SpotifyClient
.GetUserPlaylistsAsync(userId))
.Map(p => new Playlist(p.Name, Seq(p.Tracks))));
} Notice how
Then we can write generic functions that takes a runtime of any flavour, as long as it has specific constraints: static Aff<RT, Seq<Playlist>> concatPlaylists<RT>(string user1, string user2)
where RT : struct, HasSpotify, HasCancel<RT> =>
from p1 in Spotify<RT>.getUserPlaylist("123")
from p2 in Spotify<RT>.getUserPlaylist("456")
select p1 + p2; This allows for building of different runtimes that support unit-testing (for example). Or, you can just bake in the known static Aff<Runtime, Seq<Playlist>> concatPlaylists(string user1, string user2) =>
from p1 in Spotify<Runtime>.getUserPlaylist("123")
from p2 in Spotify<Runtime>.getUserPlaylist("456")
select p1 + p2; This isn't unit-testable, but removes the needs for the constraints. Then we can create a runtime, with an instance of the var runtime = new Runtime(new SpotifyAPI.SpotifyHttpClient(), default, default);
var aff = concatPlaylists<Runtime>("user123", "user456");
var result = await aff.Run(runtime);
The idea here is to create a runtime that captures all of your IO, not just the Spotify API, but file-access, configuration, etc. into one place (the runtime), and then define declarative functions (that return
There are two 'built-in' runtimes for live and test environments. Which will give you an idea of how these are built. They can't be extended, so you need to copy/paste them as a starting point, then add the capabilities that your application needs (like spotify API access). But off the bat, they have the following traits: HasCancel<Runtime>,
HasConsole<Runtime>,
HasFile<Runtime>,
HasEncoding<Runtime>,
HasTextRead<Runtime>,
HasTime<Runtime>,
HasEnvironment<Runtime>,
HasDirectory<Runtime> Which maps to a lot of the This may seem like a lot of effort for not much gain, but what you get is:
And in the next release:
There's lots of examples in the |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
Because I don't know the library you're using, I've created this mock of it:
For the simplest version of
Aff
(the non-runtime version), you can do this: