-
I'm having a play with LanguageExt.Core My code below is horrible, but hopefully it's enough to illustrate what I'm scratching my head over. I think I understand the basics when handling a Task that may throw, but I'm unclear on how best to handle things when an async method may throw, but may also return null. My inclination would be to return a type of Here's the code without using LanguageExt. It makes a call to a police data api, deserializes the response, then logs it to the console: using System.Text.Json;
namespace LanguageExtTrial;
public readonly record struct Force(string Id, string Name);
public static class PoliceData
{
private static async Task<Stream> GetForcesStream()
{
using var client = new HttpClient();
// Could throw, could return null.
var response = await client.GetAsync("https://data.police.uk/api/forces");
return await response.Content.ReadAsStreamAsync();
}
private static async Task<Force[]> DeserializeForces(Stream forces)
{
// Could throw, could return null.
return await JsonSerializer.DeserializeAsync<Force[]>(
forces,
new JsonSerializerOptions(JsonSerializerDefaults.Web));
}
public static async Task<Force[]> GetForces()
{
var stream = await GetForcesStream();
return await DeserializeForces(stream);
}
}
public class Program
{
static async Task Main()
{
Console.WriteLine(JsonSerializer.Serialize(await PoliceData.GetForces()));
}
} And with LanguageExt: using System.Text.Json;
using LanguageExt;
using LanguageExt.Common;
using static LanguageExt.Prelude;
namespace LanguageExtTrial;
public readonly record struct Force(string Id, string Name);
public static class PoliceData
{
private static IO<Stream> GetForcesStream() =>
liftIO(async () => {
using var client = new HttpClient();
var response = await client.GetAsync("https://data.police.uk/api/forces");
return await response.Content.ReadAsStreamAsync();
});
private static IO<Force[]> DeserializeForces(Stream forces) =>
liftIO(async () => {
// This could return null, but I'm not handling that case and should be.
return await JsonSerializer.DeserializeAsync<Force[]>(
forces,
new JsonSerializerOptions(JsonSerializerDefaults.Web));
});
public static IO<Force[]> GetForces() => GetForcesStream().Bind(DeserializeForces);
}
public class Program
{
static void Main()
{
// Type inference complains if I try to perform the Console.Writeline side effect in here, unless
// I return `unit` in both cases... which I guess makes sense but also seems a little ugly - so I've
// opted to return a string in both cases and perform the side effect later... not sure if this is sensible.
var res = PoliceData.GetForces()
.Match(
result => JsonSerializer.Serialize(result),
error => error.Message
)
.Run();
Console.WriteLine(res);
}
} Maybe I should be doing something like this - but at this point the nested public static class PoliceData
{
private static IO<Stream> GetForcesStream() =>
liftIO(async () => {
using var client = new HttpClient();
var response = await client.GetAsync("https://data.police.uk/api/forces");
return await response.Content.ReadAsStreamAsync();
});
private static IO<Validation<Error, Force[]>> DeserializeForces(Stream forces) =>
liftIO(async () => {
var maybeForces = await JsonSerializer.DeserializeAsync<Force[]>(
forces,
new JsonSerializerOptions(JsonSerializerDefaults.Web));
return maybeForces is null
? Fail<Error, Force[]>(Error.New("Forces was null"))
: Pure(maybeForces);
});
public static IO<Validation<Error, Force[]>> GetForces() =>
GetForcesStream().Bind(DeserializeForces);
}
public class Program
{
static void Main()
{
var res = PoliceData.GetForces()
.Match(
result => result
.Match(
Succ: success => JsonSerializer.Serialize(success),
Fail: failure => failure.Message),
error => error.Message
)
.Run();
Console.WriteLine(res);
}
} I guess my questions are:
I've left the whole "runtime" concept out of this altogether as I'm trying to take baby steps! Appreciate these are very beginner level question so thanks in advance for any guidance :) |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 2 replies
-
You're on the right track, but yeah the moment it starts feeling messy, you're likely to be veering off course (or, there's a feature from So, the Errors are handled slightly differently too. From within a
The idea is that for (types that support catching) you can use the var result = ioOperation
| @catch(Errors.Cancelled, ...)
| @catch(Errors.Timeout, ...) The various overloads allow you to map to:
They allow matching through You can also use Here's my version with the public readonly record struct Force(string Id, string Name);
public static class PoliceData
{
static IO<Stream> GetForcesStream() =>
liftIO(async e =>
{
using var client = new HttpClient();
var response = await client.GetAsync("https://data.police.uk/api/forces", e.Token);
return await response.Content.ReadAsStreamAsync(e.Token);
});
static IO<Seq<Force>> DeserializeForces(Stream forces) =>
from results in liftIO(async e =>
await JsonSerializer.DeserializeAsync<Force[]>(forces, new JsonSerializerOptions(JsonSerializerDefaults.Web), e.Token)
switch
{
null => throw new SerializationException(),
var x => x
})
select toSeq(results);
public static IO<Seq<Force>> GetForces() =>
from stream in GetForcesStream()
from forces in DeserializeForces(stream)
select forces;
}
public static class ConsoleIO
{
public static IO<Unit> WriteLine(string value) =>
IO.lift(() =>
{
Console.WriteLine(value);
return unit;
});
}
public class Program
{
static void Main() =>
MainIO.Run();
static readonly IO<Unit> MainIO =
from result in PoliceData.GetForces().Map(Serialise) | catchOf((Error e) => e.Message)
from _ in ConsoleIO.WriteLine(result)
select unit;
static string Serialise<A>(A value) =>
JsonSerializer.Serialize(value);
} NOTE: I haven't tested any of this yet (especially the resource tracking), so although this will all work when
|
Beta Was this translation helpful? Give feedback.
You're on the right track, but yeah the moment it starts feeling messy, you're likely to be veering off course (or, there's a feature from
v5
that I haven't added yet!)So, the
IO<A>
type is the 'raw unit of IO' inv5
language-ext. It doesn't have lots of features likeEff<A>
, although it is no slouch!Errors are handled slightly differently too. From within a
liftIO
your best bet (to raise errors) is to throw an exception. The code within theliftIO
is supposed to be the 'messy bit' that we capture an make nice and fluffy by wrapping it in theIO
type. Outside of theliftIO
(once you have a real IO monad) you have the opportunity to@catch
|@catchM
|@catchOf
the errors and handle them …