4 ms·
> You can map, filter, reduce, chain, compose functions that all work with optionals, and lift functions that work on numbers (or any other type) to be function
by munificent 6y ago
> You can map, filter, reduce, chain, compose functions that all work with optionals, and lift functions that work on numbers (or any other type) to be functions that work on Optional<number> but none of that is even mentioned in this piece (likely cause then its harder to justify picking nullable types instead)
Maybe I'm missing something, but I don't see how that's special to option types. Here's a set of extension methods in Dart that provide the operations you describe:
extension NullableExtensions<T> on T? {
R? map<R>(R Function(T) transform) => this == null ? null : transform(this);
T? filter(bool Function(T) predicate) {
if (this == null) return null;
if (predicate(this)) return this;
return null;
}
}
extension FunctionExtensions<T, R> on R Function(T) {
R? Function(T?) lift() => (T? param) => param == null ? null : this(param);
}
extension NullableIterableExtensions<T> on Iterable<T?> {
Iterable<R?> map<R>(R Function(T) transform) =>
this.map((e) => e == null ? null : transform(e));
Iterable<T?> filter(bool Function(T) predicate) =>
where((e) => e == null || predicate(e));
}
Chaining has built-in syntax:
int? maybeInt = ...
print(maybeInt?.isEven.toString());
I'm not sure what you mean by "compose".
Of course, this is not entirely as expressive as option types because of the inability to nest, but the article is pretty clear that nesting is the major advantage of option types.
- dragonwriter 6y ago> I'm not sure what you mean by "compose". The simplest illustration is the ability to have multiple levels of semantically meaningful optionality: Option<Option<int>> is a meaningful type, and None, Some(None), and Some(Some(1)) are all meaningfully-distinct values. int? doesn't compose to int?? and has no good way to differentiate what Option<Option<int>> represents as None and Some(None).
- contravariant 6y agoIronically the main advantage of Monads is that Option<Option<int>> has a natural transformation to Option<int>.
- marcosdumay 6y agoThe reason Option<Option<int>> isn't a problem is because it's a monad, so people will naturally transform it into Option<int> while writing their programs. There is no irony there. If it wasn't a monad, the GGP would be incorrect, and ad-hock solutions would be very valuable.
- jerf 6y agoIn a language with Haskell's guarantees, you can mechanically get from the standard "bind" function to prove that "m (m a)" can be converted to "m a" [1]. That function is called "join", and is a lesser-well known way to write monad implementations which is equivalent to the more famous bind function. (IMHO, join is a better way to understand the typeclass intuitively. The standard bind is better to program with in general use, but much harder to grok.) So, in Haskell, it's more than just "the APIs would tend to encourage not nesting the types"... having a valid monad implementation means they really are equivalent. [1]: https://stackoverflow.com/questions/3382210/monad-join-function https://stackoverflow.com/questions/3382210/monad-join-funct...
- dllthomas 6y agoThey are not equivalent. Join can throw away information - and certainly does for (Maybe (Maybe a) -> Maybe a) and ([[a]] -> [a]). They would be equivalent if there was an isomorphism, but of course there isn't.
- bern4444 6y agoBy compose I quite literally mean the compose function. const add3 = val => val + 3; const multiplyBy10 = val => val * 10; const subtract5 = val => val - 5; const doABunchOfMath = compose(add3, subtract5, multiplyBy10); const ninetyEight = doABunchOfMath(10); const optionallyNinetyEight = Some(10).map(doABunchOfMath) That Dart already provides these extensions feels like they are providing an optional type but through a more complicated mechanism than a simple class Nowhere in any of the functions I wrote above do I as a user have to worry about handling the case where no number exists, that's handled by the option type and signified by the type (The value in the option may or may not exist) and I can write my functions without ever having to worry about handling that case
- gresrun 6y agoCompose can also be implemented easily: extension NullableExtensions<T> on T? { R? map<R>(R Function(T) transform) => this == null ? null : transform(this!); } V Function(V) compose<V>(Iterable<V Function(V)> functions) => functions.reduce((composedFunction, function) { return (V value) => composedFunction(function(value)); }); num add3(num val) => val + 3; num multiplyBy10(num val) => val * 10; num subtract5(num val) => val - 5; final doABunchOfMath = compose([add3, subtract5, multiplyBy10]); final optionallyDoABunchOfMath = (num? value) => value.map(doABunchOfMath); doABunchOfMath(10); // 98 optionallyDoABunchOfMath(10); // 98 optionallyDoABunchOfMath(null); // null The nullable syntax (${Type}?) also makes it clear to readers that this is a type which may or may not contain a value. If you don't supply the trailing '?', Dart will enforce that the value must exist at compile-time and you can write your functions without worrying about nulls sneaking in where they're unwanted. In effect, int? is almost nearly Option<int>, except you cannot represent Option<Option<int>> with Dart's nullable syntax. Check out https://nullsafety.dartpad.dev/ https://nullsafety.dartpad.dev/ to play around with the possibilities!
- coldtea 6y ago>By compose I quite literally mean the compose function. Outside Lisp/Scheme has anybody used those for anything other than examples in FP tutorials? Every case I ever seen is non-production tutorial thing like "add3".
- contravariant 6y agoYou missed out the most interesting one, which is 'reduce': extension NullableIteratorExtensions<T> on Iterator<T?> { T? reduce<T>(T? Function(T,T) f, T? x0) => this.moveNext() ? (this.reduce(f, (x0 == null || this.current == null) ? null : f(x0,current))) : x0; } Not sure if this is the best notation but then I've never written Dart before. Of course this is all a bit easier to read if we use some syntax specially built for Monads such as Haskell's do syntax: reduce f (head:tail) x0 = do { x <- x0 ; y <- head ; reduce f tail (f x y) } reduce f [] x0 = x0 which has the advantage that it works for all Monads.