Skip to content

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

Merged
merged 7 commits into from
Mar 27, 2018

Conversation

FeepingCreature
Copy link
Contributor

@FeepingCreature FeepingCreature commented Feb 15, 2018

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 awkward thingy.isNull ? Nullable!SomeType.init : op(thingy.get) with thingy.bind!op.

"Monads are underrated."

@dlang-bot
Copy link
Contributor

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 verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the annotated coverage diff directly on GitHub with CodeCov's browser extension
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

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 references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Copy link
Member

@wilzbach wilzbach left a 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 into std.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))))
Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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
Copy link
Member

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

Copy link
Member

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);
}

Copy link
Member

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);
}
Copy link
Member

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.

Copy link
Contributor Author

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.
Copy link
Member

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
Copy link
Member

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);
}
-----
Copy link
Member

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)

Copy link
Contributor Author

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..?

Copy link
Member

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.

@wilzbach
Copy link
Member

Also note that NullableRef already has a bind method.

@JackStouffer
Copy link
Member

IMO this would be better setup as a method of Nullable.

@FeepingCreature
Copy link
Contributor Author

FeepingCreature commented Feb 16, 2018

I thought you couldn't do templated methods with alias parameters that take closures. (For no clear reason.)

@MetaLang
Copy link
Member

I don't think bind is a good name, because NullableRef already has a bind method that does something different, and because it only makes sense in the context of a Haskell's particular implementation of Monadic containers. The (more or less) equivalent in D is a range that you apply a map function to, but Nullable is not a range (though it could easily be given a range interface).

@JackStouffer
Copy link
Member

apply?

@FeepingCreature
Copy link
Contributor Author

FeepingCreature commented Feb 16, 2018

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.

@MetaLang
Copy link
Member

@JackStouffer's suggestion sounds fine to me.

@baklava151
Copy link

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.

@MetaLang
Copy link
Member

The main problem with Nullable (and it has many problems) is that it is not like the Maybe monad. It implicitly converts to the underlying value which, it turns out, causes all sorts of problems when you try to use Nullable seriously.

@FeepingCreature
Copy link
Contributor Author

I try my level best to never use that functionality.

@FeepingCreature
Copy link
Contributor Author

Renamed to apply.

{
return n.bind!(i => i * i);
}
-----
Copy link
Member

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.

@DmitryOlshansky
Copy link
Member

DmitryOlshansky commented Feb 19, 2018

Think it'd be interesting if they were added to Phobos at some point. Still, a good start.

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

@jmdavis
Copy link
Member

jmdavis commented Feb 19, 2018

I'd be very leery about treating Nullable as a range. Conceptually, I'd consider a Nullable and a range to be quite different, though I can see an argument for treating them similarly. However, it gets particularly iffy if Nullable is wrapped around a range, since then the range API on the Nullable suddenly wins out over the range API on the wrapped type, which could break code.

@FeepingCreature
Copy link
Contributor Author

FeepingCreature commented Feb 19, 2018

This only breaks code because Phobos is horribly broken in the first place in not forcing programmers to call .get explicitly. If anything, that's what should - must - be changed.

@jmdavis
Copy link
Member

jmdavis commented Feb 19, 2018

This only breaks code because Phobos is horribly broken in the first place in not forcing programmers to call .get explicitly.

Maybe having get is a design flaw, but the fact of the matter is that it's there, so we have to deal with that.

@FeepingCreature
Copy link
Contributor Author

FeepingCreature commented Feb 19, 2018

Having get is fine, the horrible misfeature is alias get this.

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.

@MetaLang
Copy link
Member

@jmdavis that's a good point that I had not considered... maybe an idiomatic D implementation of an Option-like range should actually invert the API such that the traditional member functions an Option type would have are just free functions that work on ranges. I think this would square better with Andrei's concerns that we already have std.range.Only which is basically Some!T.

@FeepingCreature
Copy link
Contributor Author

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;
Copy link
Member

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

@JackStouffer
Copy link
Member

@FeepingCreature Can you please email @andralex (email can be found at https://erdani.com/index.php/contact/) about reviewing this name addition? Thanks.

@andralex
Copy link
Member

Wait a second. The title says add bind but the code adds apply. What gives?

I'm okay with adding apply but as a method of Nullable, not an alembicated free function.

Copy link
Member

@andralex andralex left a 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.

@FeepingCreature
Copy link
Contributor Author

FeepingCreature commented Mar 22, 2018

Gladly, just as soon as that actually works.

struct S
{
    void apply(alias fun)() { fun(); }
}

void main()
{
    int k = 5;
    S().apply!(() => k);
}

Error: template instance `apply!(delegate () => k)` cannot use local __lambda1 as
parameter to non-global template apply(alias fun)()`

@JackStouffer JackStouffer changed the title Implement bind for Nullable, in analogy to Haskell's monad operation Implement apply for Nullable, in analogy to Haskell's monad operation Mar 23, 2018
@FeepingCreature
Copy link
Contributor Author

@andralex ping

@JackStouffer
Copy link
Member

I'm going to stick my neck out here and move forward with this.

@andralex Feel free to revert if you disagree.

@andralex
Copy link
Member

Cool. How about taking auto ref? Presumably some functions might want to modify their argument.

@dlang-bot dlang-bot merged commit 0c9a10b into dlang:master Mar 27, 2018
@FeepingCreature
Copy link
Contributor Author

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?

@andralex
Copy link
Member

@FeepingCreature the only problem is we're not disallowing functions that take ref. Ideally we'd either disallow them or let them run with the expected behavior. Right now the results may be surprising.

Generally: principles of FP apply slightly differently (and not always elegantly) to languages with mutation.

@wilzbach
Copy link
Member

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)
I proposed a few things at #6178 (comment) (I don't care which one is picked).

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.

@JackStouffer
Copy link
Member

Sounds like a good topic for a DIP.

@andralex
Copy link
Member

For new free entities great, for methods marking something as unstable is more difficult. Perhaps we could add a new attribute @unstable.

@wilzbach
Copy link
Member

Can we please instead of sticking our necks out, have an experimental stage for new functions?

-> #6389 (a start anyhow)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants