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

Direct type class syntax over concrete data types #811

Merged
merged 78 commits into from
May 21, 2018

Conversation

raulraja
Copy link
Member

@raulraja raulraja commented Apr 22, 2018

The following PR adds a syntax DSL for all data types that arrow exposes type class instances for.

This DSL allows users to not worry about knowing if binding, flatMap etc are in Monad, or what any other combinator they plan on using belongs to a certain type class.

It eliminates the need to explicitly refer to the instances inside the data type companion.

Until now users where forced to do things like:

Option.applicative().map(Option(1), Option(1), Option(1), { (a, b, c) -> a + b + c }) //Option(3)
Option.monad().binding {
  val a = Option(1).bind()
  val b = Option(a + 1).bind()
  a + b
} 
// Option(3)
Option.functor()...
Option.traverse()...

Instead with the syntax DSL most used type classes that the data type provides instances for are bundled as the single this scope of the contained syntax function.
The scope contains all extension functions and static functions that the type classes define and are available to use within the user provided block.

import arrow.instances.*

Option syntax { //`this` is Monad, Applicative, Functor, Traverse, etc...
  binding { ... }
  map(....)
  traverse(...)
}

Either<String>() syntax { //`this` is Monad, Applicative, Functor, Traverse, etc...
  binding { ... }
  map(....)
  traverse(...)
}

The docs contain now instructions for users wanting to provide a similar DSL for their custom data type and it looks like this:

If you defined your own instances over your own data types and wish to use a similar syntax DSL you can do so for both types with a single type argument such as Option:

object OptionContext : OptionMonadErrorInstance, OptionTraverseInstance {
  override fun <A, B> Kind<ForOption, A>.map(f: (A) -> B): Option<B> =
    fix().map(f)
}

infix fun <A> Option.Companion.syntax(f: OptionContext.() -> A): A =
  f(OptionContext)

Or for types that require partial application of some of their type arguments such as Either<L, R> where L needs to be partially applied

class EitherContext<L> : EitherMonadErrorInstance<L>, EitherTraverseInstance<L> {
  override fun <A, B> Kind<EitherPartialOf<L>, A>.map(f: (A) -> B): Either<L, B> =
    fix().map(f)
}

class EitherContextPartiallyApplied<L> {
  infix fun <A> syntax(f: EitherContext<L>.() -> A): A =
    f(EitherContext())
}

fun <L> Either(): EitherContextPartiallyApplied<L> =
  EitherContextPartiallyApplied()

@raulraja raulraja requested a review from a team April 22, 2018 01:20
fun <L> Either(): EitherContextPartiallyApplied<L> =
EitherContextPartiallyApplied()
```

If you're defining your own instances and would like for them to be discoverable in their corresponding datatypes' companion object, you can generate it by annotating them as `@instance`, and Arrow's [annotation processor](https://github.com/arrow-kt/arrow#additional-setup) will create the extension functions for you.
Copy link
Member

Choose a reason for hiding this comment

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

This block belongs to the Instances section, before the Syntax one.

@raulraja raulraja changed the title WIP - Direct type class syntax over concrete data types Direct type class syntax over concrete data types May 16, 2018
@raulraja raulraja requested a review from a team May 16, 2018 18:00
}

fun <L> Either(): EitherContextPartiallyApplied<L> =
EitherContextPartiallyApplied()
Copy link
Member

@pakoito pakoito May 16, 2018

Choose a reason for hiding this comment

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

Can we hide the partial implementation?

  interface ContextPartiallyApplied<T> {
    infix fun <A> syntax(f: T.() -> A): A
  }

fun <L> Either() =
  object: ContextPartiallyApplied<EitherContext<L>> {
    infix fun <A> syntax(f: EitherContext<L>.() -> A): A =
      f(EitherContext())
  }

Copy link
Member Author

Choose a reason for hiding this comment

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

yep

@pakoito
Copy link
Member

pakoito commented May 16, 2018

To align with the existing documentation I'd rather the DSL objects were suffixed Syntax rather than Context.

@pakoito
Copy link
Member

pakoito commented May 16, 2018

Another suggestion I can think of is to move the functions to the companion object of the witness types. That way we can get consistent and unambiguous syntax across types, i.e. ForEither() syntax { } and ForOption() syntax {}.

@raulraja
Copy link
Member Author

@pakoito Many types use now syntax that are not kinded and have no witness. For example:

String syntax {
  "a".combine("b")
}
// ab

@pakoito
Copy link
Member

pakoito commented May 17, 2018

Here's a snippet that reflects my proposal:

Definition:

object ListKSyntax : Syntax<ListKSyntax>, ListKMonadInstance, ListKTraverseInstance, ListKMonoidKInstance {
  override fun <A, B> Kind<ForListK, A>.map(f: (A) -> B): ListK<B> =
    fix().map(f)
    
  override infix fun <A> syntax (f: ListKSyntax.() -> A): A =
      f(ListKContext)
}
operator fun <A> ForListK.Companion.invoke(): Syntax<ListKSyntax> =
  ListKSyntax

Usage:

ForOption() syntax {
  just(1).sequence(ForListK())
}

For platform types like String or Boolean we will create a new object prefixed with For. This also solves the problem where the platform type Boolean doesn't have a companion object and we need a workaround

object BooleanInstances {
fun show(): Show<Boolean> =
object : BooleanShowInstance {}
fun eq(): Eq<Boolean> =
object : BooleanEqInstance {}
}

@ersin-ertan
Copy link
Contributor

ersin-ertan commented May 17, 2018

Proposal to change syntax to instances

import arrow.instances.*

Option instances { //`this` is Monad, Applicative, Functor, Traverse, etc...
  binding { ... }
  map(....)
  traverse(...)
}
  • instances plural, when paired with a data type named as a singular ex.Option instances will hopefully be enough to avoid naming clashes with the OOP concept of instantiation(though one may still wrongly assume that a list of instances is being returned)
  • instances acts like special version of Kotlins scoped function with(...), allowing you to call the functions of the data type's available instances, so instead of writing withAvailableInstancesOf(Option) { ... }, the word instances provides enough context for the representation

@pakoito
Copy link
Member

pakoito commented May 17, 2018

What about extensions? ForOption() extensions { ... }

@nomisRev
Copy link
Member

nomisRev commented May 18, 2018

I'm a little unsure about instances because when I open a ObservableK instances block I'd expect to also have access to effects instances

@raulraja
Copy link
Member Author

raulraja commented May 20, 2018

@nomisRev that has access to the Effect instance.

ForObservableK instances { 
  // `this` is also an `Effect<ForObservableK>`
}

https://github.com/arrow-kt/arrow/pull/811/files#diff-088c779bf125135ce8dbe42b7af6d477R119

@nomisRev
Copy link
Member

Ah yeah of course. That data type only lives in effects... My bad :D

I meant if I manually add a instance. Or typeclasses/instances defined in Optics, Helios,..

@raulraja
Copy link
Member Author

@nomisRev For those cases we are providing instructions to users as part of this PR that tells them how to create the same DSL including their custom instances:
https://github.com/arrow-kt/arrow/pull/811/files#diff-f6d7de9e73be8613678763d3780567e2R152

@raulraja
Copy link
Member Author

raulraja commented May 21, 2018

@pakoito @ersin-ertan @nomisRev
Seems most people I talked to prefer syntax or extensions over instances. also @pakoito proposed prefixing all types with For to work around missing companions for example in Boolean.

Does this look ok to everyone and a fair compromise for now?

ForOption extensions { ... }
ForIO extensions { ... }
ForBoolean extensions { ... }
ForEither<String>() extensions { ... }

@nomisRev
Copy link
Member

Looks good to me! Also thanks for pointing out the entry in docs 👍

Copy link
Contributor

@ersin-ertan ersin-ertan left a comment

Choose a reason for hiding this comment

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

Looks good. Clever re-usage of For... too

Copy link
Member

@pakoito pakoito left a comment

Choose a reason for hiding this comment

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

For... extensions gets my green!

@raulraja raulraja merged commit 84dbd62 into master May 21, 2018
@raulraja raulraja deleted the rr-datatypes-syntax-blocks branch May 21, 2018 21:39
RawToast pushed a commit to RawToast/kategory that referenced this pull request Jul 18, 2018
* `Const.run(String.monoid) { ... }` + `with(Const, String.monoid()) { ... }`

* `Either.run { ... }` + `with(Either) { ... }`

* `Eval.run { ... }` + `with(Eval) { ... }`

* `Function0.run { ... }` + `with(Function0) { ... }`

* `Function1.run<I> { ... }` + `with<I>(Function1) { ... }`

* `Id.run { ... }` + `with(Id) { ... }`

* `Byte.run { ... }` + `with(Byte) { ... }`

* `Double.run { ... }` + `with(Double) { ... }`

* `Int.run { ... }` + `with(Int) { ... }`

* `Long.run { ... }` + `with(Long) { ... }`

* `Short.run { ... }` + `with(Short) { ... }`

* `Float.run { ... }` + `with(Float) { ... }`

* `Option.run { ... }` + `with(Option) { ... }`

* `Either<L>().run { ... }` + `with(Either<L>()) { ... }`

* `Const(String.monoid()).run { ... }` + `with(Const(String.monoid())) { ... }`

* `Function1<I>().run { ... }` + `with(Function1<I>()) { ... }`

* `String.run { ... }` + `with(String) { ... }`

* `Try.run { ... }` + `with(Try) { ... }`

* `Coproduct(TF, TG).run { ... }` + `with(Coproduct(TF, TG)) { ... }`

* `EitherT(MF).run { ... }` + `with(EitherT(MF)) { ... }`

* `Ior(SL).run { ... }` + `with(Ior(SL)) { ... }`

* `Kleisli(MF).run { ... }` + `with(Kleisli(MF)) { ... }`

* `ListK.run { ... }` + `with(ListK) { ... }`

* `MapK<Key>().run { ... }` + `with(MapK<Key>()) { ... }`

* `NonEmptyList.run { ... }` + `with(NonEmptyList) { ... }`

* `OptionT(MF).run { ... }` + `with(OptionT(MF)) { ... }`

* `SequenceK.run { ... }` + `with(SequenceK) { ... }`

* `SetK.run { ... }` + `with(SetK) { ... }`

* `SortedMapK<Key>().run { ... }` + `with(SortedMapK<Key>()) { ... }`

* `StateT(ME).run { ... }` + `with(StateT(ME)) { ... }`

* `Validated(SL).run { ... }` + `with(Validated(SL)) { ... }`

* EitherT type arg name fixes

* `WriterT(MF, MW).run { ... }` + `with(WriterT(MF, MW)) { ... }`

* mtl: `Const(MA).run { ... }` + `with(Const(MA)) { ... }`

* mtl: `Function1<I>().run { ... }` + `with(Function1<I>()) { ... }`

* mtl: `Kleisli<F, D, E>(ME).run { ... }` + `with(Kleisli<F, D, E>(ME)) { ... }`

* mtl: `ListK.run { ... }` + `with(ListK) { ... }`

* mtl: `Option.run { ... }` + `with(Option) { ... }`

* mtl: `OptionT(MF).run { ... }` + `with(OptionT(MF)) { ... }`

* mtl: `StateT(ME).run { ... }` + `with(StateT(ME)) { ... }`

* mtl: `WriterT(MF, MW).run { ... }` + `with(WriterT(MF, MW)) { ... }`

* mtl: `DeferredK.run { ... }` + `with(DeferredK) { ... }`

* mtl: `FlowableK.run { ... }` + `with(FlowableK) { ... }`

* mtl: `ObservableK.run { ... }` + `with(ObservableK) { ... }`

* mtl: `IO.run { ... }` + `with(IO) { ... }`

* mtl: `Fix(FF).run { ... }` + `with(Fix(FF)) { ... }`

* Replaced `run` with infix `syntax`

* Ank docs for `Either` and `EitherT` using `syntax` DSL

* Ank docs for `NonEmptyList` using `syntax` DSL

* Ank docs for `Option` using `syntax` DSL

* Ank docs for `Option` and `OptionT` using `syntax` DSL

* Ank docs for `SequenceK` using `syntax` DSL

* Ank docs for `State` using `syntax` DSL

* Ank docs for `StateT` using `syntax` DSL

* Ank docs for `Try` using `syntax` DSL

* Ank docs for `IO` using `syntax` DSL

* Ank docs for `@product` using `syntax` DSL

* Ank docs for `DeferredK` using `syntax` DSL

* Ank docs for `ObservableK` using `syntax` DSL

* Ank docs for DI using `syntax` DSL

* Ank docs for Error Handling using `syntax` DSL

* Ank docs for Glossary using `syntax` DSL

* Ank docs for `MonadError` using `syntax` DSL

* Ank docs for `MonadFilter` using `syntax` DSL

* Ank docs for `Monoid` using `syntax` DSL

* Ank docs for `Order` using `syntax` DSL

* Ank docs for `Semigroup` using `syntax` DSL

* Ank docs for `Show` using `syntax` DSL

* Progress toward refactoring tests

* data types syntax in `arrow-free` and related tests

* code review

* `syntax` -> `extensions`

* `For` prefix
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

Successfully merging this pull request may close these issues.

4 participants