diff --git a/changelog/std-typecons-nullable-apply.dd b/changelog/std-typecons-nullable-apply.dd new file mode 100644 index 00000000000..575cbf4a7f3 --- /dev/null +++ b/changelog/std-typecons-nullable-apply.dd @@ -0,0 +1,15 @@ +`apply` was added to `std.typecons`. + +`apply` is an operation for $(REF Nullable, std, typecons) values that "unpacks" the `Nullable`, performs some +operation (that is passed as a template parameter), then packs the result into another `Nullable` if necessary. +When the initial `Nullable` is `null`, the resulting `Nullable` is also `null` and the function is not +called. + +----- +Nullable!int n; +alias square = i => i * i; +n = n.apply!square; // does nothing if isNull +assert(n.isNull); +n = 2; +assert(n.apply!square.get == 4); +----- diff --git a/std/typecons.d b/std/typecons.d index 32d06356f9d..2222cc23d52 100644 --- a/std/typecons.d +++ b/std/typecons.d @@ -3353,6 +3353,102 @@ auto nullable(alias nullValue, T)(T t) assert(ntts.to!string() == "2.5"); } +/** +Unpacks the content of a $(D Nullable), performs an operation and packs it again. Does nothing if isNull. + +When called on a $(D Nullable), $(D apply) will unpack the value contained in the $(D Nullable), +pass it to the function you provide and wrap the result in another $(D Nullable) (if necessary). +If the Nullable is null, $(D apply) will return null itself. + +Params: + t = a $(D Nullable) + fun = a function operating on the content of the nullable + +Returns: + `fun(t.get).nullable` if `!t.isNull`, else `Nullable.init`. + +See also: + $(HTTP en.wikipedia.org/wiki/Monad_(functional_programming)#The_Maybe_monad, The `Maybe` monad) + */ +template apply(alias fun) +{ + import std.functional : unaryFun; + + auto apply(T)(T t) + if (isInstanceOf!(Nullable, T) && is(typeof(unaryFun!fun(T.init.get)))) + { + alias FunType = typeof(unaryFun!fun(T.init.get)); + + enum MustWrapReturn = !isInstanceOf!(Nullable, FunType); + + static if (MustWrapReturn) + { + alias ReturnType = Nullable!FunType; + } + else + { + alias ReturnType = FunType; + } + + if (!t.isNull) + { + static if (MustWrapReturn) + { + return fun(t.get).nullable; + } + else + { + return fun(t.get); + } + } + else + { + return ReturnType.init; + } + } +} + +/// +nothrow pure @nogc @safe unittest +{ + alias toFloat = i => cast(float) i; + + Nullable!int sample; + + // apply(null) results in a null $(D Nullable) of the function's return type. + Nullable!float f = sample.apply!toFloat; + assert(sample.isNull && f.isNull); + + sample = 3; + + // apply(non-null) calls the function and wraps the result in a $(D Nullable). + f = sample.apply!toFloat; + assert(!sample.isNull && !f.isNull); + assert(f.get == 3.0f); +} + +/// +nothrow pure @nogc @safe unittest +{ + alias greaterThree = i => (i > 3) ? i.nullable : Nullable!(typeof(i)).init; + + Nullable!int sample; + + // when the function already returns a $(D Nullable), that $(D Nullable) is not wrapped. + auto result = sample.apply!greaterThree; + assert(sample.isNull && result.isNull); + + // The function may decide to return a null $(D Nullable). + sample = 3; + result = sample.apply!greaterThree; + assert(!sample.isNull && result.isNull); + + // Or it may return a value already wrapped in a $(D Nullable). + sample = 4; + result = sample.apply!greaterThree; + assert(!sample.isNull && !result.isNull); + assert(result.get == 4); +} /** Just like $(D Nullable!T), except that the object refers to a value