Skip to content
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

Problem: We can not chain top level functions on ONE like we can chain on MANY #166

Closed
kasperpeulen opened this issue Jan 5, 2019 · 11 comments

Comments

@kasperpeulen
Copy link

kasperpeulen commented Jan 5, 2019

I have the following top level functions:

String capitalize(String s) =>
    s.isEmpty ? s : '${s[0].toUpperCase()}${s.substring(1)}';
String exclaim(String s) => '$s!';
String smile(String s) => '$s😁';
String cheer(String s, {int times = 1}) => '$s${'🍻' * times}';
String beg(String s, {int times = 1}) => '$s${'🙏' * times}';
String greet(String s) => 'Hello Dart Team.\n${s}';
String end(String s) => '$s\n';

Say that I now want to perform those functions on "many" strings.
With that I mean, on a variable that implements Iterable<String>.
In that case I am allowed to "chain" those functions in an easy to read and easy to write way.
In this way the chronological order of the code is preserved.

Iterable<String> emphasizeMessages(Iterable<String> messages) => messages
    .map(capitalize)
    .map(greet)
    .map(exclaim)
    .map(smile)
    .map((it) => beg(it, times: 3))
    .map((it) => cheer(it, times: 2))
    .map(end)
      ..forEach(print); // even chain side effects if needed

main() {
  emphasizeMessages([
    'I love to chain',
    'this allows to read in chronological order',
  ]);
}

However, say now that I would like to do the exact same thing on "one" string.
Now the most trivial implementation becomes hard to read and even harder to write.

String emphasizeMessage(String message) => end(
    cheer(beg(smile(exclaim(greet(capitalize(message)))), times: 3), times: 2));

main() {
  print(emphasizeMessage('I love to chain'));
}

There all kind of ways to make this a little better to read. But all of them have disadvantages, and none is so elegant as when working with Iterables.

There are some solutions proposed for this. For example, the pipeline operator (#43) and extension methods (#41). I think there is an other solution which is more in line with the Dart language than the pipeline operator, and could exist next to extension methods (or even be implemented like an extension method).

This is inspired by Kotlin's let extension method. For Dart a good name may be the do method. It would look like this:

String emphasizeMessage(String message) => message
    .do(capitalize)
    .do(greet)
    .do(exclaim)
    .do(smile)
    .do((it) => beg(it, times: 3))
    .do((it) => cheer(it, times: 2))
    .do(end);

I will also open another specific feature issue for this do or let method that could be implemented by top level type Object.

@andreashaese
Copy link

Would these functions not better live as methods inside String (given the existence of class extensions)? Then, chaining them would seem very natural to me.

With the suggested Object.do: I don't really know Kotlin, but to me it seems that let is defined as a method of the called object's type. If in Dart do was added to Object, would 42.do(exclaim) be valid? I'm not aware of self constraints in Dart (other than, to a degree, Mixins). You could do runtime type checks, but that's a lot of boilerplate, and besides, autocomplete would become a mess.

If you want to compose free functions today, you could btw do something like this:

C Function(A) compose<A, B, C>(B Function(A) f, C Function(B) g) => (a) => g(f(a));
T Function(T) composeAll<T>(List<T Function(T)> fn) => fn.reduce(compose);

String emphasizeMessage(String message) => composeAll([
      capitalize,
      greet,
      exclaim,
      smile,
      (String it) => beg(it, times: 3),
      (String it) => cheer(it, times: 2),
      end
    ])(message);

@Zhuinden
Copy link

Zhuinden commented Jan 5, 2019

Let is the following:

inline fun <T, R> T.let(transform: (T) -> R) = transform(this)

So it lets you run a block of code that will map the current object to something else, then you can continue chaining at the end of the block.

@andreashaese
Copy link

Exactly, hence my comment about Object.do: Kotlin defines the function on T, which could be any type, e.g. String or int), whereas the proposed solution defines the do function on the base class (Object) of all objects:

You could certainly do something like R do<T, R>(R Function(T) transform) but how would you enforce that T is the type of the to-be-transformed object? That's what I mean with "self constraint": a constraint expressing this is of type T during static analysis, which afaik is not possible in Dart. The only option I see is to type-check during runtime, but that makes it inefficient, and – in my opinion much worse – you lose all type checking guarantees from the compiler.

Consider this example:

abstract class Base {
  R map<R, T>(R Function(T) transform) {
    if (this is T) {
      return transform(this as T);
    } else {
      return null;
    }
  }
}

class A extends Base {}
class B extends Base {}

B mapAtoB(A a) => B();
A mapBtoA(B a) => A();

main() {
  final a = A();
  final b = a.map(mapAtoB);
  assert(a != null); // true as expected
  assert(b != null); // true as expected
  final whatsthis = a.map(mapBtoA); // type mismatch, but compiles just fine
  assert(whatsthis != null); // assertion failure
}

It "works", but I think the bar should be much higher for such a language feature: Not only is the type mismatch not obvious because it compiles without warnings, but also when you trigger autocorrect after typing in a.map(, you'll see all available unary functions, rendering autocomplete rather useless. Also, 42.map(exclaim).

Now maybe there is another way and I'm just not experienced enough in Dart, in that case I would love to stand corrected!

@andreashaese
Copy link

There is one more option from functional programming land: you can define a simple box functor that will allow you to chain functions in a type-safe manner:

class Box<T> {
  const Box(this.value);
  final T value;
  Box<S> map<S>(S Function(T) f) => Box(f(this.value));
}

String emphasizeMessage(String message) => Box(message)
  .map(capitalize)
  .map(greet)
  .map(exclaim)
  .map(smile)
  .map((String it) => beg(it, times: 3))
  .map((String it) => cheer(it, times: 2))
  .map(end)
  .value;

void main() {
  print(emphasizeMessage('hello'));
}

That's why it works for arrays: they are functors. An arbitrary class isn't, but you can always simply wrap it inside one.

@Zhuinden
Copy link

Zhuinden commented Jan 5, 2019

...maybe all we need is the Id monad? 🤔

@andreashaese
Copy link

There all kind of ways to make this a little better to read. But all of them have disadvantages, and none is so elegant as when working with Iterables.

I'd disagree that function composition or functors/monads are inelegant, in fact I think they are the correct solution here. But the question still stands whether this is considered "good enough": it's not uncommon to want such a thing, but this requires knowledge of more advanced functional programming techniques.

I do not think that Object.do is a good idea for the given reasons, but one could argue that being able to implement something similar to Kotlin's let might be helpful even if the function itself wasn't part of a standard library.

The way I see it, there are two impediments:

  1. You can't currently extend existing types.
  2. You can't extend generic types.

Personally, and independent of the original problem, I'm very much in favor of having 1) in Dart, and I'd also be interested in 2).

I'm coming from Swift, and there you also can't extend generic types, but you can achieve something similar. For those interested, here's a very simple Swift implementation:

protocol Mappable {}

extension Mappable {
    func map<R>(_ f: (Self) -> R) -> R { return f(self) }
}

// Retrofit existing types (String, Int etc. are structs, so can't add conformity via a base class)
extension String: Mappable {}
extension Int: Mappable {}

func greet(name: String) -> String { return "Hello \(name)" }
func addTwo(number: Int) -> Int { return number + 2 }
func stringify(number: Int) -> String { return String(number) }

"Bob".map(greet) // "Hello Bob"
42.map(addTwo) // 44
1000.map(stringify).map(greet) // "Hello 1000"
"Bob".map(addTwo) // error: cannot convert value of type '(Int) -> Int' to expected argument type '(String) -> _'

@eernstg
Copy link
Member

eernstg commented Jan 7, 2019

I think the idea of adding a method to Object is interesting—even though it's basically an impossible move to make, because of the massive potential breakage.

But let's pretend for a minute that we would actually be able to add the following method (and let's pretend that we can call it let). Let's also pretend that we have a type called This which may be used in instance members of a class and which denotes the class of the value of this. The result could be a quite nice fit for Dart, and rather expressive, too.

// Yep, it's _that_ class Object.
class Object {
  ... // Existing stuff.
  X let<X>(X Function(This) f) => f(this);
}

This is similar to the extension method (as in Kotlin, and as it might be expressed in an extension of Dart where some sort of extension methods are supported), and it will allow the same level of static typing. For instance, the following would work just fine:

String emphasizeMessage(String message) => message
    .let(capitalize)
    .let(greet)
    .let(exclaim)
    .let(smile)
    .let((it) => beg(it, times: 3))
    .let((it) => cheer(it, times: 2))
    .let(end);

At each step, type inference will ensure that the type argument String is passed to let, which is then also the type of the invocation, which is the static type of the receiver for the next let. That's an example of what I call 'the same level' of static typing as that of the Kotlin extension method. We'd also get the desired failure, of course:

int addTwo(int i) => i + 2;
var _ = 'Another string'.let(addTwo); // Compile-time error.

The error arises because the argument to let has static type X Function(String), and inference cannot select any value for X which will make int Function(int) assignable to that type.

On top of this, we'd also get the ability to use let on receivers of type dynamic, and we'd get the relevant type check at run-time, because This denotes the actual type of the receiver, and not just the statically known one.

An aside:

Note that the self-type This is not a particularly fancy concept in Dart, because Dart already has existential types all over the place (because of generic covariance). Also note that we can actually emulate This by repeating the declaration of let in all classes (or "every class where we care about having a better type than the one we get from some superclass for which we do have a declaration"):

class A {
  X let<X>(X Function(A) f) => f(this); // Add this to emulate `This` in the signature.
  ... // Rest of A.
}

Obviously, if we really want this feature and we don't want to introduce a full-fledged self-type then we can let the compiler generate such methods.

Similarly, if we want to avoid the breakage then we could call it do, because that's a reserved word, and it won't break anything at all to allow it after a ., if we can make the parser happy about it. ;-)

@andreashaese
Copy link

I like This (pun intended)! Could be beneficial elsewhere too, e.g.

abstract class num {
  // rest
  bool operator ==(This other); // <, <=, >, >=
  This operator +(This other); // -, *
  This abs(); // etc.
}

@munificent
Copy link
Member

Today, you can just do:

String emphasizeMessage(String message) => [message]
    .map(capitalize)
    .map(greet)
    .map(exclaim)
    .map(smile)
    .map((it) => beg(it, times: 3))
    .map((it) => cheer(it, times: 2))
    .map(end)
    .first;

That doesn't seem too bad to me.

@gamebox
Copy link

gamebox commented Jan 18, 2019

I think that if you are going to wrap it in an array, that the proposed Box functor above is much more elegant, and more obvious in its intent. It's more functional than idiomatic Dart tends to be, but very clear. I think the do or let method on Object seems much more Dart-y, but I would be sad if Dart took a little swerve towards a more functional style 😄

@kasperpeulen
Copy link
Author

This issue is solved by extension methods.

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

No branches or pull requests

6 participants