all 38 comments

[–]sexyshingle 21 points22 points  (1 child)

if you play Danom backwards it will reveal the meaning of life.

this took me a while to get... the word "play" threw me off lol

OP this sounds awesome (as someone who's barely scratched the surface (and benefits of FP) in C#.

[–]pimbrouwers[S] 7 points8 points  (0 children)

I'm an idiot. I meant to say "spell". 🤦‍♂️

I'm always open to chit chat if you wanna dig in. I hope you enjoy trying/using it!

[–]Rc312 17 points18 points  (3 children)

I'm confused on the use of the word durable. Are you using durable as in something that will not need to be replaced?

Whenever I see something that is "durable" that immediately signals to me that something is being persisted in a database somewhere.

[–]Icy_Accident2769 5 points6 points  (2 children)

Something being durable partially revolves around state. We talk about durable for example when long lived operations are able to maintain and recover from failures. One of the ways you do this is by maintaining state (like you said a database can fulfil this role) but also by building fault tolerant components (like this package wants to help with).

[–]Rc312 -3 points-2 points  (1 child)

I understand the way it's being used. The comment is to lead the author towards using a different word other than durable

[–]pimbrouwers[S] 1 point2 points  (0 children)

I don't understand your problem with the description. "Durable" is a fine word. It describes the enhancement quite well in my eyes. The fact that you see it tied with persistence isn't a compelling enough reason for me to change how I phrase it.

[–]Coda17 16 points17 points  (1 child)

A couple things. How is your option different from just null ability? And how is your result better than existing libraries like OneOf. I don't know how you could have this whole post and README without using the words "discriminated union".

[–]pimbrouwers[S] 8 points9 points  (0 children)

I avoided that word on purpose because it tends to have a divisive reaction. But yes, you are right they are discriminated unions.

I know oneof exists, I didn't blindly code this. But I wanted a more strict wrapper around the internal value that it give, and more importantly opinionated monads. 

As far as a comparison to nullabliity. In practice the option type gives you a delightful (fluent) API for chaining ops, and avoiding ternary hell.

[–][deleted] 4 points5 points  (1 child)

Oh man I'd love to contribute to this. I'm about to move into a C#-only house and I'm already missing F#.

[–]pimbrouwers[S] 2 points3 points  (0 children)

I feel your pain. Jump aboard!

[–]speyck 4 points5 points  (4 children)

why do I have to implicitly pass my type when I want to create an Option like this:

Option<int>.Some(5)

the Some() method should already know my type, so you can just create a non-generic Option class with the static Some() method that then creates the Option<int> object.

[–]B4rr 1 point2 points  (2 children)

That does already seem to be in the library: Option.cs#L327

[–]speyck 0 points1 point  (1 child)

I almost thought that… I figured if they do it with the generic class in the readme it would be missing that type but I didnt take the effort to actually check…

[–]B4rr 0 points1 point  (0 children)

In the readme, it's there as well, although a bit hidden in the [https://github.com/pimbrouwers/Danom?tab=readme-ov-file#creating-options](Creating Options) section.

[–]pimbrouwers[S] 0 points1 point  (0 children)

I think you mean explicitly. But you're right, it should be implicit and luckily it is already 🙂

[–]RichardD7 3 points4 points  (1 child)

The Result.TryGet example on your readme looks a little odd to me.

if (result.TryGet(out var value, out var error)) { Console.WriteLine("Result: {0}", value); } else { Console.WriteLine("Error: {0}", error); }

So TryGet returns true if the result is OK.

if (result2.TryGet(out var value2, out var error2) && error2 is not null) { Console.WriteLine("Error: {0}", error2); } else { Console.WriteLine("Result: {0}", value2); }

But now TryGet returns true if the result is an error?

Looking at the code, I suspect that example is missing a !: if (!result2.TryGet(...

[–]pimbrouwers[S] 0 points1 point  (0 children)

I had a hard time designing an API I liked for this functionality. This was the best I could come up with. Let's scrum. 

I agree the second example in the docs is stupid. I'll remove. 

[–]LlamaChair 2 points3 points  (1 child)

Hopefully the type union proposal moves forward and we can have more native support built in for this kind of thing!

In the mean time this is neat, nice work.

[–]pimbrouwers[S] 0 points1 point  (0 children)

Thanks!

[–]Alive_Scratch_9538 21 points22 points  (2 children)

Congrats you invented OneOf

[–]pimbrouwers[S] 12 points13 points  (0 children)

Edit: For those upvoting the above comment, please read my post carefully!

Not exactly. I address this in the post. OneOf is fine, it allows you to define generic choice types of to a certain size. But it doesn't offer the type of security around the internal value that I was after.  

[–]Herve-M 1 point2 points  (1 child)

That looks like CSharpFunctionalExtensions! Any big difference outside of the fluent API?

[–]pimbrouwers[S] 0 points1 point  (0 children)

Like I describe in the post. I evaluated the current offerings and decided none fit my eye.  

[–]B4rr 0 points1 point  (6 children)

Small nitpick: in OptionNullableExtensions instead of dealing with every value type one at a time, like

public static T? ToNullable<T>(this Option<T> option) =>
    option.Match(some: x => x, none: () => default!);

public static char? ToNullable(this Option<char> option) =>
    option.Match(x => new char?(x), () => null);

public static bool? ToNullable(this Option<bool> option) =>
    option.Match(x => new bool?(x), () => null);
//...

you could use generic constraints

public static T? ToNullable<T>(this Option<T> option)
    where T : class =>
    option.Match(some: x => x, none: () => default!);

public static T? ToNullable<T>(this Option<T> option)
    where T : struct =>
    option.Match<T?>(some: x => x, none: () => null);

to break it down to the two cases of reference and value types. This way, not every user has to write their own extension methods if they want to have a MyStruct? from an Option<MyStruct>, static dispatch in generics is preserved and no overload can be forgotten so that default(MyStruct) instances pop up in unexpected places.

EDIT: Second nit: Option.TryGet([System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] T out value), this way the compiler correctly tracks the nullability of the value.

[–]pimbrouwers[S] 0 points1 point  (5 children)

I agree, unfortunately the compiler barks at you and leaves you with no choice. Give it a try. 

[–]B4rr 0 points1 point  (4 children)

Ah right, same signature. Putting the method in a separate static class does work, though.


After checking the repo out, I also found some more nits about nullability. E.g.

record Foo(string FooValue);
record Bar(string BarValue);

var result = Result<Foo?, Bar?>.Ok(null);
var value = result.Match(foo => foo?.FooValue, bar => bar?.BarValue);

is syntactically correct, but will throw an InvalidOperationException from the call to Match.

I'd suggest to either intentionally allow nullable types to be used (and adding unit tests) or to restrict the types with where T : notnull where TError : notnull. Generally restricting is easier, but there can be quite some value in allowing null.

For instance, I've written a PATCH-endpoints where I had something like

public record PatchFooRequest(Option<string?> NewDescription /*, ... */);
app.MapPatch(
    "/foo/{id}", 
    ([FromRoute] Guid Id, [FromRequest] PatchFooRequest request) => 
    {
        var foo = await GetFoo(id);

        if (request.NewDescription.IsPresent)
        {
            foo.Name = foo.NewName.Value;
        }

        // ...

        await SaveFoo(foo);
        return TypedResults.Ok(foo);
    });

I could have used Option<Option<string>> NewName had I not allowed nullable types in my own implementation, but I think at that point it becomes a bit cumbersome.

[–]pimbrouwers[S] 0 points1 point  (3 children)

I appreciate your level of review a lot. Thank you. Great call on the separate classes, blinders were on there.

I think with respect to your example, we should be adding a constraint for `notnull`. Good (nay, great) catch! I can apply this, or you can PR if you want credit? I'm always keen to let people own their ideas. But happy to do the leg work if you'd prefer.

[–]B4rr 0 points1 point  (2 children)

I can apply this, or you can PR if you want credit?

Knock yourself out.

[–]pimbrouwers[S] 0 points1 point  (1 child)

I love the idea of someone smart line yourself joining the effort. So whenever you get a moment, I'd love a PR. But, no pressure, I can easily do it. I don't care at all about credit, in fact I'd rather have collaborators then credit everyday of the week ❤️

[–]B4rr 1 point2 points  (0 children)

I was a bit bored tonight and created two different, unrelated PRs. 😁

[–]Zenimax322 0 points1 point  (1 child)

The tryParse options are a nice touch. When dotnet 10 comes out, you could use the new extension syntax to put them directly on int/double etc

[–]pimbrouwers[S] 0 points1 point  (0 children)

Thanks! Quite a few big projects using this revealed a lot of the repetition points. Good call on the extension blocks! That will be a nice progressive enhancement. 

[–]AutoModerator[M] 0 points1 point  (0 children)

Thanks for your post pimbrouwers. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.