Skip to content

Conversation

@JakobOvrum
Copy link
Contributor

Modeled after Scala's scala.Option type. It's been discussed on the forums before, I found at least this thread.

(I'll move it to std.experimental.typecons once #2945 is merged, as it establishes the module)

@dlang-bot
Copy link
Contributor

dlang-bot commented Jan 10, 2016

Fix Bugzilla Description
12679 std.typecons.Maybe

@JakobOvrum
Copy link
Contributor Author

I also have these:

Option!T left(T, U)(auto ref Algebraic!(T, U) algebraic)
{
    if(auto p = algebraic.peek!T)
        return some(*p);
    else
        return option(none);
}

Option!T right(T, U)(auto ref Algebraic!(T, U) algebraic)
{
    if(auto p = algebraic.peek!U)
        return some(*p);
    else
        return option(none);
}

Not sure if I should include them in this PR.

edit:

I guess an API that adds an Option!T-returning function to VariantN is better. But I digress.

std/typecons.d Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be moved to std.traits? If they are helpful here then the chances of them being helpful to someone else is high.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it's outside the scope of this PR. They can be moved later. (I think std.allocator also has its own isCopyable template)

@JakobOvrum JakobOvrum force-pushed the std_typecons_option branch 2 times, most recently from 14ec3a5 to d17d365 Compare January 12, 2016 01:43
@JakobOvrum
Copy link
Contributor Author

Fixed linker error and added Option.orElse.

@JakobOvrum
Copy link
Contributor Author

Option!T now works for const and immutable types T (including as a range), with the limitation that it's only reassignable to none, it cannot be assigned to another Option!T.

const Option!T and immutable Option!T are usable to some extent as well, but they're not ranges as ranges need to be poppable.

std/typecons.d Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foreach

Why not use $(D foreach). I thought that that was the standard. Same applies everywhere else

$(REF each, std, algorithm, iteration)

this didn't link for some reason

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the same as $(D …).

REF is from dlang/dlang.org/pull/1184.

@MetaLang
Copy link
Member

Is there any reason you don't specialize Option on types that have a null value and use null in place of defined?

std/typecons.d Outdated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is inout not usable here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it is not, that's why it's structured like it is.

(edit: this way also makes the Algebraic import lazy, while still keeping it in the documentation as the return type)

@JakobOvrum
Copy link
Contributor Author

@MetaLang, this way supports some(null), i.e. a non-empty null value, just like the Scala type. Option replaces null used to mean absence of an optional value, which doesn't cover every use of null.

edit:

e.g.:

auto arr = [null, new Object];
auto maybeFront = arr.frontOption;
assert(maybeFront.assumeNonEmpty == null);

std/typecons.d Outdated
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const and immutable overloads are untested because Algebraic does not support non-mutable types...

@andralex
Copy link
Member

What is the relationship between this and only? At first sight it looks like a non-empty Option is only(x) and an empty Option is only(T.init).drop. If that's the case, we'd save a lot of trouble.

@JakobOvrum
Copy link
Contributor Author

Most of the work here is not duplicated by only. Option!T makes an effort to properly support any type of T, including non-mutable, non-copyable, non-default-constructable and nested types. Also, as Option!T is named and suitable for long-living variables (globals, TLS, member fields etc.), it destroys the held value when going non-emptyempty. Idioms for working with optional values such as getOrElse and frontOption are also added and tested.

I think the best way would be to make unary only return Option!T. It is important that the type is named so it can be defined and initialized separately (typically in a [module] constructor) and show up in documentation when used as a return type. only(a) would be the same as some(a), so the latter could be removed (and then it might make sense to move Option to std.range).

@JakobOvrum JakobOvrum force-pushed the std_typecons_option branch from 4b8f5b5 to f7e13ea Compare March 14, 2016 09:42
@JakobOvrum
Copy link
Contributor Author

Fixed compilation failure with latest DMD. Option!T is now only sliceable when T is copyable.

@JakobOvrum JakobOvrum force-pushed the std_typecons_option branch 3 times, most recently from f3c62a6 to 938deb5 Compare March 14, 2016 11:26
@JakobOvrum
Copy link
Contributor Author

Added Option!V lookup(T : V[K], V, K)(T assocArray, auto ref K key) as another fundamental artifact for working with option types. The name isn't particularly good, suggestions welcome.

@ntrel
Copy link
Contributor

ntrel commented Mar 21, 2016

I'm not convinced that Option should itself be a range. It makes code less clear that it's dealing with Options, and moreover front disguises the escape of a non-existent value. I know empty should be checked first for ranges, but when reading someone else's code you don't know if the range is just assumed to be non-empty.

I would much prefer all access to the value is null/assert-safe or uses some clear method like assumeNonEmpty. Option can still have a range method, but common things like map, filter probably should be added as Option methods.

Added Option!V lookup(T : V[K], V, K)(T assocArray, auto ref K key)

I think a version that returns an Option!(V*) is needed to replace in. Also, I don't think all functions producing an option need to go in std.typecons, lookup should go in std.array IMO.

@JakobOvrum
Copy link
Contributor Author

I'm not convinced that Option should itself be a range. It makes code less clear that it's dealing with Options, and moreover front disguises the escape of a non-existent value. I know empty should be checked first for ranges, but when reading someone else's code you don't know if the range is just assumed to be non-empty.

Range code that assumes non-empty is wrong. Don't use Option.front directly either, it's just for passing to range algorithms.

Option being a range is the whole point. It's the same in Scala where it has been very successful.

I think a version that returns an Option!(V*) is needed to replace in. Also, I don't think all functions producing an option need to go in std.typecons, lookup should go in std.array IMO.

How would Option!(V*) be used? If the contained pointer is nullable then the whole exercise is meaningless. The current lookup replaces in.

@ntrel
Copy link
Contributor

ntrel commented Mar 22, 2016

How would Option!(V*) be used? If the contained pointer is nullable then the whole exercise is meaningless. The current lookup replaces in.

How do you modify *(k in aa) with your lookup? I'm suggesting to wrap option(k in aa). If the result is not empty, the pointer, though nullable, would never be null unless modified by the caller. It could even return an Option of a wrapped pointer that can't be reassigned, guaranteeing the pointer is never null.

std/typecons.d Outdated

auto values = only(false, true, false).map!getX.map!option;
assert(values[0].empty);
assert(!values[1].empty && *values[1].front == 42);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use Option.front directly either, it's just for passing to range algorithms.

Please replace front here with something else (as this appears in the docs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I ate my own dog food and rewrote it to use equal.

@JakobOvrum
Copy link
Contributor Author

Yeah, you're right, this is too similar to unary only to separate them. I'll rework the PR on top of std.range and see how it turns out.


I noticed that std.range.takeOne is essentially frontOption, although we have no equivalent of backOption. From a clean slate I would have preferred [front|back]Option, but in the name of consistency, maybe takeOne[Back] wouldn't be the end of the world. This assumes that [front|backOption] carry their own weight anyway; it's probably better to keep the initial PR minimal and submit them as follow-ups, I've just been using this PR to brainstorm ideas about option types in D.


@ntrel, the current lookup does prevent double lookup but it doesn't provide the by-ref advantage of in. I was under the impression that option(v) dereferenced pointers but it doesn't and that's probably a good thing. Maybe lookup should return Option!(V*) for V[K].

@JakobOvrum JakobOvrum force-pushed the std_typecons_option branch from 938deb5 to 53e157b Compare April 2, 2016 14:07
@JakobOvrum JakobOvrum changed the title Add std.typecons.Option Add std.range.Option Apr 2, 2016
@JakobOvrum JakobOvrum force-pushed the std_typecons_option branch 2 times, most recently from ccb42c9 to 33facb1 Compare April 2, 2016 14:43
Modeled after Scala's scala.Option type.

Fix ticket 12679
@JakobOvrum JakobOvrum force-pushed the std_typecons_option branch from 33facb1 to c778381 Compare April 2, 2016 14:49
@JakobOvrum
Copy link
Contributor Author

  • Moved to std.range
  • Removed some and changed unary std.range.only to return Option!T
  • Fixed @andralex's concerns about wasteful moves and swaps (and added space after if where it was missing)

About takeOne - it's a higher order range that supports accessing the front of the underlying range by reference, so it's not compatible with frontOption... it's an interesting difference.


About lookup - wrapping option(key in aa) is not useful, just write that as-is. What lookup provides is:

auto aa = ["foo": 42];
assert(aa.lookup("foo").equal(only(42)));
// instead of
assert(option("foo" in aa).map!(p => *p).equal(only(42)));

This is the same inconvenience faced by any function that returns a null pointer to mean absence of value, such as VariantN.peek, so maybe a general solution for T*Option!T is better.


No-one has said anything about it, but this PR still includes the arguably cargo-culty NoneType and none for explicit emptying. An alternative would be to just use default-initialization for empty construction, and popFront for emptying post-construction. I guess one issue is that popFront should not be called on an empty range while assigning to none is currently valid regardless of emptiness. Any thoughts?


I'm aware of the build failure, I'll investigate.

@ntrel
Copy link
Contributor

ntrel commented Apr 2, 2016

NoneType

I suppose none could be made a property so it doesn't have a named type.

maybe a general solution for T* → Option!T is better

Yes, we could have:

Option!T deref(T)(T*);
Option!T deref(T)(Option!(T*));

(I still think lookup should be in std.array, not sure what others think. Hopefully returning Option will become widespread across Phobos).

@JakobOvrum
Copy link
Contributor Author

Alright, here are the real differences between Option!T and OnlyResult!(T, 1):

  • Option!T gives access to its held value by reference, which has many subtle implications, including supporting non-copyable types. OnlyResult!(T, n) gives access only by value, which is by design (the idea was to prevent sort(only(3, 1, 2)) from compiling because it does not work as expected).
  • Because OnlyResult!(T, 1) also gives access by value, it can store the held value as Unqual!T while still being a range of T - but this relies on is(Unqual!T : T), which can be false for user-defined types; e.g.immutable(S) where struct S { int* p; }. This seems like a design flaw.

Code in std.path relies on OnlyResult!(immutable char, 1) being reassignable (to itself), while Option!(immutable char) isn't. So far I haven't been able to reconcile this difference, but I'll give it another go later.


auto values = only(false, true, false).map!getX.map!option;
assert(values[0].empty);
assert(values[1].map!(p => *p).equal(only(42)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could add an assert for values[1].equal(only(getX(true))) before this line. (And if we add deref, we can use it instead of map here and on line 7044).

@ntrel
Copy link
Contributor

ntrel commented Apr 12, 2016

Because OnlyResult!(T, 1) also gives access by value, it can store the held value as Unqual!T while still being a range of T - but this relies on is(Unqual!T : T), which can be false for user-defined types; e.g.immutable(S) where struct S { int* p; }.

BTW I'm working on Rebindable!(inout S) which could solve these kind of problems. I think it's @safe and will hopefully submit it to Phobos. See https://github.com/ntrel/stuff/blob/master/rebindstruct.d.

edit: See #4363

@wilzbach
Copy link
Contributor

Merge failed - needs rebase :)

@andralex
Copy link
Member

rebase pls

@wilzbach
Copy link
Contributor

wilzbach commented on Apr 27
Merge failed - needs rebase :)
rebase pls

Are u still alive @JakobOvrum? ;-)
Should we introduce a new category of "orphaned" PRs that are just lacking minor fixes and thus can be adopted by anyone?

@andralex
Copy link
Member

I see one way to push this forward: add a none function right after the existing only such that none!int returns an Only!int with nothing in it. This would cover the vast majority of uses of this PR.

@JackStouffer
Copy link
Contributor

No activity from PR author in nine months. Closing.

@andralex
Copy link
Member

Convert this to an enhancement request?

@JackStouffer
Copy link
Contributor

@s-ludwig
Copy link
Member

@andralex

I see one way to push this forward: add a none function right after the existing only such that none!int returns an Only!int with nothing in it. This would cover the vast majority of uses of this PR.

Could you imagine renaming Only!T to one of Option!T/Optional!T/Maybe!T in the long term? It makes sense to go this way on a technical level, but seeing a declaration void foo(Only!T t), one can hardly guess that this actually means that t is optional.

@andralex
Copy link
Member

Hmmm... neither name is very descriptive. The type really means "ZeroOrMore" whereas "Only" suggests exactly one and "Option" and "Optional" imply "zero or one". "Maybe" is arguably better. "Some" would be great but evokes "one or more" and promises to be a great source of confusion with Haskell's some. The same link suggests that "many" would be "zero or more".

"Bag" is a term I'd like - you have a "bag" of things, which may be empty, contain one item, or more items.

It's also a bummer that only!int is really zero ints...

@s-ludwig
Copy link
Member

Oh, I didn't realize that only can take multiple values! Then it doesn't really make sense of course. alias Option(T) = OnlyResult!(T, 1); could in theory work, although a proper type would be better in terms of documentation and generic code.

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.

10 participants