-
-
Notifications
You must be signed in to change notification settings - Fork 740
Implement apply for Nullable, in analogy to Haskell's monad operation #6182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement apply for Nullable, in analogy to Haskell's monad operation #6182
Conversation
Thanks for your pull request and interest in making D better, @FeepingCreature! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. |
9c43129
to
b235ecd
Compare
b235ecd
to
680e5c7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for proposing this.
I added a few comments. In general: getting a new function into Phobos is hard, so be prepared for thorough reviews and making a strong case for its addition.
I made an initial pass with mostly nits.
BTW this rebinds me about safeDeref
that @MetaLang posted a while ago on the forums: https://forum.dlang.org/post/[email protected]
Other random ideas:
- any chance this could work with any kind of
null
pointers, e.g. classes? (then it could go intostd.functional
) - there's
std.experimental.typecons
, but it's more or less a graveyard (at the moment) - there's a discussion about
orElse
for ranges. I know, not the same, but as ideally all functions would magically work together so I thought it's worth mentioning
std/typecons.d
Outdated
{ | ||
import std.functional : unaryFun; | ||
|
||
auto bind(T)(T t) if (isInstanceOf!(Nullable, T) && is(typeof(unaryFun!fun(T.init.get)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://dlang.org/dstyle.html#phobos_declarations
Constraints on declarations should have the same indentation level as their declaration:
std/typecons.d
Outdated
{ | ||
import std.meta : Alias; | ||
|
||
alias toFloat = Alias!(i => cast(float) i); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alias toFloat = i => cast(float) i
(same for the other uses)
std/typecons.d
Outdated
|
||
sample = 3; | ||
f = sample.bind!toFloat; | ||
assert(!sample.isNull && !f.isNull); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert(f.get == 3.0)
(similar for the examples below)
std/typecons.d
Outdated
} | ||
|
||
/// | ||
@safe unittest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add nothrow pure @nogc
too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this is quite a long example for user-facing docs. Split up into at least two and add comments.
std/typecons.d
Outdated
result = sample.bind!greaterThree; | ||
assert(!sample.isNull && !result.isNull); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: typically functions in Phobos are separated by only one line
std/typecons.d
Outdated
sample = 4; | ||
result = sample.bind!greaterThree; | ||
assert(!sample.isNull && !result.isNull); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be great to see more tests/examples here, e.g. pipes of bind
and other real world examples to make your PR and case stronger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why you'd want to pipe it. It's really just a convenience function for !foo.isNull ? fn(foo.get) : Nullable!(typeof(fn(typeof(foo.get).init))).init.
@@ -0,0 +1,13 @@ | |||
`bind` was added to std.typecons. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Backticks for std.typecons too?
@@ -0,0 +1,13 @@ | |||
`bind` was added to std.typecons. | |||
|
|||
$(REF bind, std, typecons) is an operation for $(D Nullable) values that "unpacks" the $(D Nullable), performs some |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mix of style. New Phobos code uses backticks and you even you use them in the title ;-)
Also it makes sense to use $(REF Nullable, std, typecons)
{ | ||
return n.bind!(i => i * i); | ||
} | ||
----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that this is a good example on why bind
is useful.
Sure, it shows what it does, but a user reading the release documentation will ask themselves: how can I use this in my code?
Nullable!int n;
n = n.bind!log10; // does nothing if isNull
assert(n.isNull);
n = 100;
assert(n.bind!log10.get == 2);
(this is subjective of course)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mostly just wanted to show the syntax. log10 is reals, not ints..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's essentially a map
operation. I think otherwise considering Nullable!T as a range of 0 or 1 elements is much cleaner design.
Also note that |
IMO this would be better setup as a method of Nullable. |
I thought you couldn't do templated methods with alias parameters that take closures. (For no clear reason.) |
I don't think |
|
I had originally called it map, but I thought that might be too confusing. Should I change it to map? It is sort of like map if you think of a Nullable as a range that may have a length of 0 or 1. |
@JackStouffer's suggestion sounds fine to me. |
More analagous to Haskell's Maybe monad (well, exactly like Haskell's Maybe monad), monads overall are more general than that. Think it'd be interesting if they were added to Phobos at some point. Still, a good start. |
The main problem with |
I try my level best to never use that functionality. |
Renamed to apply. |
{ | ||
return n.bind!(i => i * i); | ||
} | ||
----- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's essentially a map
operation. I think otherwise considering Nullable!T as a range of 0 or 1 elements is much cleaner design.
Instead of introducing a foreign concept, let's just reuse the range analogy. In fact range API + std.algorithm/std.range may work like a monad to an extent |
I'd be very leery about treating |
This only breaks code because Phobos is horribly broken in the first place in not forcing programmers to call |
Maybe having |
Having Anyway, if we could treat Nullable as a range (or both as Monads) that'd be great, but we can't, so apply should be fine I guess. |
@jmdavis that's a good point that I had not considered... maybe an idiomatic D implementation of an |
Ping? |
std/typecons.d
Outdated
Nullable!int sample; | ||
|
||
// apply(null) results in a null $(D Nullable) of the function's return type. | ||
auto f = sample.apply!toFloat; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for demonstration, change auto
to Nullable!float
@FeepingCreature Can you please email @andralex (email can be found at https://erdani.com/index.php/contact/) about reviewing this name addition? Thanks. |
Wait a second. The title says add I'm okay with adding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make a method.
Gladly, just as soon as that actually works.
|
@andralex ping |
I'm going to stick my neck out here and move forward with this. @andralex Feel free to revert if you disagree. |
Outdated
Cool. How about taking |
Since this is pretty much copied from Haskell's monads, it's very much a pure functional approach, meaning you're supposed to construct a new Nullable and assign it instead of modifying in-place. I feel like if you're modifying the parameter, you may be doing the idiom wrong. Not sure if that's a sufficient reason not to do it? |
@FeepingCreature the only problem is we're not disallowing functions that take Generally: principles of FP apply slightly differently (and not always elegantly) to languages with mutation. |
Can we please instead of sticking our necks out, have an experimental stage for new functions? Rust is showing the way here by making news additions "unstable" first and then stabilizing them after this testing period, e.g. https://blog.rust-lang.org/2018/02/15/Rust-1.24.html (they do the same thing for language features too: https://doc.rust-lang.org/unstable-book) However, any mechanism that allows us to mark it unstable for one or two versions is a lot better than immediately releasing it and releasing that we have made a mistake / slight oversight which happens far too frequently. |
Sounds like a good topic for a DIP. |
For new free entities great, for methods marking something as unstable is more difficult. Perhaps we could add a new attribute |
-> #6389 (a start anyhow) |
bind
is an operation for Nullables that unpacks the nullable, if it's non-null, and then repacks the result in another appropriately-typed nullable. This replaces a lot of awkwardthingy.isNull ? Nullable!SomeType.init : op(thingy.get)
withthingy.bind!op
."Monads are underrated."