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

Add Representable Functor #2284

Merged
merged 6 commits into from
Jul 9, 2018

Conversation

eli-jordan
Copy link
Contributor

@eli-jordan eli-jordan commented Jun 7, 2018

I have been working on adding representable functors to cats.

This PR adds;

  • The Representable typeclass.
  • The RepresentableStore data type, which is a generalisation of the Store comonad.

@codecov-io
Copy link

codecov-io commented Jun 7, 2018

Codecov Report

Merging #2284 into master will increase coverage by 0.02%.
The diff coverage is 98.11%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2284      +/-   ##
==========================================
+ Coverage   95.03%   95.05%   +0.02%     
==========================================
  Files         338      343       +5     
  Lines        5878     5931      +53     
  Branches      210      220      +10     
==========================================
+ Hits         5586     5638      +52     
- Misses        292      293       +1
Impacted Files Coverage Δ
testkit/src/main/scala/cats/tests/CatsSuite.scala 70% <ø> (ø) ⬆️
core/src/main/scala/cats/instances/tuple.scala 100% <100%> (ø) ⬆️
...rc/main/scala/cats/laws/discipline/Arbitrary.scala 98.96% <100%> (+0.05%) ⬆️
core/src/main/scala/cats/data/package.scala 88.88% <100%> (+1.38%) ⬆️
...ore/src/main/scala/cats/syntax/representable.scala 100% <100%> (ø)
...s/src/main/scala/cats/laws/RepresentableLaws.scala 100% <100%> (ø)
...cala/cats/laws/discipline/RepresentableTests.scala 100% <100%> (ø)
core/src/main/scala/cats/instances/function.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/data/Kleisli.scala 97.05% <100%> (+0.12%) ⬆️
core/src/main/scala/cats/Representable.scala 100% <100%> (ø)
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b77c388...c31445e. Read the comment docs.

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

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

this looks interesting to me. I was not aware of it.

I'll try to think about the tailRecM.

@@ -7,7 +7,8 @@ import cats.arrow._
/**
* Represents a function `A => F[B]`.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

can we revert the style changes in this file?

}
}

def representableIsBimonad[F[_], R](implicit R: Representable.Aux[F, R], M: Monoid[R]): Monad[F] = new Bimonad[F] with StackSafeMonad[F] {
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 we only get Monad if there is no Monoid[R]? Should we make that a lower priority implicit?

Copy link
Contributor Author

@eli-jordan eli-jordan Jun 8, 2018

Choose a reason for hiding this comment

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

Yes thats right. I wasn't sure how to prioritise the implicits when I wrote that instance, but just read up on it. I added RepresentableInstances1 and RepresentableInstances0

R.tabulate(a => R.index(f(R.index(fa)(a)))(a))

// TODO: Can we make this monad stack safe?
// override def tailRecM[A, B](a: A)(f: A => F[Either[A, B]]): F[B] = ???
Copy link
Contributor

Choose a reason for hiding this comment

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

I think something like this works:

R.tabulate { r: R =>
  @annotation.tailrec
  def loop(a: A): B = 
    R.index(f(a))(r) match {
      case Right(b) => b
      case Left(a) => loop(a)
    }

  loop(a)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I hadn't got around to thinking this through. That solution works.

@@ -23,7 +23,7 @@ class MonadSuite extends CatsSuite {
forAll(smallPosInt) { (max: Int) =>
val (result, aggregation) = incrementAndGet.whileM[Vector](StateT.inspect(i => !(i >= max))).run(0)
result should ===(Math.max(0, max))
aggregation should === ( if(max > 0) (1 to max).toVector else Vector.empty )
aggregation should === ( if (max > 0) (1 to max).toVector else Vector.empty )
Copy link
Contributor

Choose a reason for hiding this comment

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

can we revert style changes?

@@ -39,13 +39,13 @@ class KleisliSuite extends CatsSuite {
checkAll("Semigroupal[Kleisli[Option, Int, ?]]", SerializableTests.serializable(Semigroupal[Kleisli[Option, Int, ?]]))

Copy link
Contributor

Choose a reason for hiding this comment

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

can we revert the style changes? I'd prefer a PR with style only changes so we can focus on logic/design.


type Pair[A] = (A, A)
checkAll("Pair[String, String] <-> Boolean => String", RepresentableTests[Pair, Boolean].representable[String])

Copy link
Contributor

Choose a reason for hiding this comment

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

can we check the monad/comonad laws on a Representable 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.

I added a check for Bimonad

@eli-jordan eli-jordan changed the title WIP: Add Representable Functor Add Representable Functor Jun 9, 2018
@eli-jordan eli-jordan changed the title Add Representable Functor WIP: Add Representable Functor Jun 11, 2018
@eli-jordan eli-jordan force-pushed the representable-functor branch 2 times, most recently from 3b014ea to 7e54efb Compare June 12, 2018 11:37
@eli-jordan eli-jordan changed the title WIP: Add Representable Functor Add Representable Functor Jun 12, 2018
@eli-jordan
Copy link
Contributor Author

eli-jordan commented Jun 12, 2018

@johnynek @tpolecat @kailuowang I think this is ready for a final review / approval

* A generalisation of the Store comonad, for any `Representable` functor.
* `Store` is the dual of `State`
*/
final case class RepresentableStore[F[_], S, A](fa: F[A])(val index: S)(implicit R: Representable.Aux[F, S]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would rather not use the second parameter for index. I think scala does not use that for equality (only the first item is checked) and I doing think we want that. Also, I’d rather take R as an implicit parameter at the callsites that use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All methods in the class depend on the Representable instance, thats why I shifted it be a constructor parameter. The general reason (that I have seen) to move implicit constraints to methods rather than constructor params, it to avoid over constraining methods that do not require the implicit. I guess the downside is that if additional methods are added, that don't use the Representable instance, they will be over constrained. Is it worth moving the constraint in this case?

I will remove the second parameter list for index, it just made the implementation slightly cleaner.

@kailuowang
Copy link
Contributor

@eli-jordan it looks like there are some duplicated commits created when you merge master to your branch. You might need to hard reset to master and cherry-pick cd82d38 to produce a clean PR.

}

private[cats] sealed abstract class RepresentableInstances1 {
def catsRepresentableIsMonad[F[_], R](implicit Rep: Representable.Aux[F, R]): Monad[F] = new RepresentableMonad[F, R] {
Copy link
Contributor

Choose a reason for hiding this comment

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

This and the one above are not implicit and not much point to be imported and thus no need to follow the implicits naming convention. If we want them to be available for auto derivation of Bimonad and Monad we need to put them in a trait extendable by cats.implicits and better have some tests for potential conflicts. I vote for just rename them to better searchable if there aren't many real world use cases for them.

@eli-jordan
Copy link
Contributor Author

@kailuowang I have made the suggested change, and cleaned up the commit history.


type Pair[A] = (A, A)

checkAll("Id[Int] <-> Unit => String", RepresentableTests[Id, Unit].representable[String])
Copy link
Contributor

Choose a reason for hiding this comment

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

another nit: Id[String]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch! :-)

implicit val bimonadInstance = Representable.bimonad[Pair, Boolean]
checkAll("Pair[Int]", BimonadTests[Pair].bimonad[Int, Int, Int])
checkAll("Bimonad[Pair]", SerializableTests.serializable(Bimonad[Pair]))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

would you test the monad as well?

Copy link
Contributor Author

@eli-jordan eli-jordan Jun 23, 2018

Choose a reason for hiding this comment

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

I didn't because BimonadTests runs the laws for both Monad and Comonad plus some others and the implementation is shared. Happy to add one for monad directly though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for adding it. It wasn't a big deal, but just one more red spot on the coverage report that can always cost some attention in the future. Also it would be nice to have it covered in case
someone for whatever reason changes the monad implementation.

* Given a functorial computation on the index `S` peek at the value in that functor.
*/
def experiment[G[_]](fn: S => G[S])(implicit G: Functor[G]): G[A] = {
G.map(fn(index))(peek)
Copy link
Contributor

Choose a reason for hiding this comment

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

mind to add a test for this method? I think a doctest would probably suffice.

@eli-jordan
Copy link
Contributor Author

@kailuowang Thanks for the review. I have updated based on your comments.

@LukaJCB
Copy link
Member

LukaJCB commented Jun 23, 2018

I'm super late to this PR and I apoligize for that. I've never used representable functors before, but I took a look at the Haskell package you mentioned at the top and I see that it implies a Distributive instead of just Functor, is there a reason why you didn't choose to use our Distributive instead? :)

@eli-jordan
Copy link
Contributor Author

eli-jordan commented Jun 23, 2018

@LukaJCB Because you can actually derive distribute for any Representable

distributeRep :: (Representable f, Functor w) => w (f a) -> f (w a)
distributeRep wf = tabulate (\k -> fmap (`index` k) wf)

I guess I could have made trait Representable extends Distributive { .... But if I do that, should I do the same for Monad?

I added the derived Monad and Bimonad instance, but did not add the Distributive because I figured it could be added at a later date if desired.

@LukaJCB
Copy link
Member

LukaJCB commented Jun 23, 2018

Sounds good 👍 We can add it later :)

kailuowang
kailuowang previously approved these changes Jun 25, 2018
@kailuowang
Copy link
Contributor

Looking good to me. I am not very familiar with the practical use of it but the API looks clean and straightforward. Will be great to have two other review approvals.

@kailuowang
Copy link
Contributor

@johnynek any other issues?

def index(implicit R: Representable.Aux[F, R]): R => A = R.index(fa)
}

final class TabulateOps[A, R](f: R => A) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not extend AnyVal here and above?

Copy link
Contributor

Choose a reason for hiding this comment

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

If there is a reason can we comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When I first wrote it, I had the ops class taking implicit params and AnyVal doesn't allow multiple param lists. I missed adding it back when I changed that, will fix that up now.

kailuowang
kailuowang previously approved these changes Jul 5, 2018
new IndexOps[F, A, R](fa)
}

final class IndexOps[F[_], A, R](val fa: F[A]) extends AnyVal {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this have R on the class? Maybe it doesn’t matter but it seems like it should be on index.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my tests it didn't seem to make a difference, but I think you're right, it makes more sense to put it on index. I'll make that change now.

Copy link
Contributor

@johnynek johnynek left a comment

Choose a reason for hiding this comment

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

left a comment about possibly adding Representable for Eval.

@@ -77,6 +77,18 @@ package object cats {
override def isEmpty[A](fa: Id[A]): Boolean = false
}

/**
* Witness for: Id[A] <-> Unit => A
Copy link
Contributor

Choose a reason for hiding this comment

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

we could add something similar for Eval[A] potentially.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 , Since this has been blocking 1.2.0 release, I propose we tackle that in a separate PR
I added an issue #2320

Copy link
Member

@LukaJCB LukaJCB left a comment

Choose a reason for hiding this comment

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

👍

@kailuowang kailuowang merged commit 186dbab into typelevel:master Jul 9, 2018
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.

5 participants