jcpetruzza / barbies

BSD 3-Clause "New" or "Revised" License
92 stars 15 forks source link

Couple things that might be useful to have #39

Open masaeedu opened 2 years ago

masaeedu commented 2 years ago

Hello there. I was wondering if you'd be amenable to adding the following classes (and ways of deriving them):

type Lens' s a = Lens' { project :: forall f. Functor f => (a -> f a) -> s -> f s }

type ProductLike :: (k -> Type) -> Type
class ProductLike t
  where
  lenses :: t (Lens' (t Identity))

and:

type BTraversable1 :: (k -> Type) -> Type
class BTraversable t => BTraversable1 t
  where
  btraverse1 :: Apply e => (forall a. f a -> e (g a)) -> b f -> e (b g) 

There might be some way to turn ProductLike in a sensible way (perhaps involving Co) but based on my rushed thinking so far I haven't been able to think of anything.

jcpetruzza commented 2 years ago

I considered something like lenses in the past (maybe for any functor, not just Identity?) but I'm not sure I see usages that are not covered well by generic-lens already. Do you have scenarios where you'd like to operate on all lenses at once?

For btraverse1, would one need a new class? E.g., can't one derive it from btraverse and an auxiliary newtype wrapper that pretends an Apply is actually an Applicative, or something like that?

masaeedu commented 2 years ago

For btraverse1, would one need a new class? E.g., can't one derive it from btraverse and an auxiliary newtype wrapper that pretends an Apply is actually an Applicative, or something like that?

@jcpetruzza That makes sense at a high level, but I haven't been able to make it work. I think the relevant newtype is MaybeApply f a = Either (f a) a (where Apply f => Applicative (MaybeApply f)). But in order to convert a traversal over MaybeApply f into a traversal over f itself, you end up needing an operation Apply f => Either (f (b g)) (b g) -> f (b g), and I'm honestly not sure how to supply that in general for a "TraversableB HKD with at least one position".

You can instead make it work with something like this as the primitive operation:

bhead :: b f -> Some b

or even:

brefute :: b VoidF -> Void

But this is a bit more mysterious for an implementor.

jcpetruzza commented 2 years ago

I think I wasn't really very clear, sorry! What I meant is that since "Apply is Applicative without pure", and btraverse requires an Applicative but doesn't use pure, we can use a newtype wrapper to pretend an Apply is an Applicative whose pure will never be called. Something like this:

newtype CantUsePure f a = CantUsePure (f a)
  deriving newtype Functor

instance Apply f => Applicative (CantUsePure f) where
  pure = error "Can't use pure!"
  CantUsePure f <*> CantUsePure x = CantUsePure (f <.> x)

btraverse1
  :: (TraversableB b, Apply e)
  => (forall a . f a -> e (g a))
  -> b f
  -> e (b g)
btraverse1 h
  = coerce . btraverse (CantUsePure . h)

Of course, one shouldn't export CantUsePure.

I think I rather not introduce a dependency just for this type of functions; and one could put them in a separate package, anyway

masaeedu commented 2 years ago

@jcpetruzza The main issue as I understand it is that the quality of "not using pure" isn't attached to f, but to b. In a context where you're abstract in b, and want to demand that b has at least one "position" where f occurs, there doesn't seem to be any safe way to get around requiring a subclass of TraversableB (although the best way to encode its operations is debatable).

Of course we can just use the error technique to bypass the type system and pretend that any TraversableB is Traversable1B, and then blow up at runtime if we've made a mistake. This doesn't seem ideal though, and I'd rather not lie to the consumer of my API in this way.

jcpetruzza commented 2 years ago

You are correct, if you have something that is an Apply and not an Applicative, this method would work as a backdoor only if you control the instances on which you use it. I thought it would always work with the generic instances we provide, but this is not true, since it will currently blow on types that have values not under f.

I still think that there is no difference between TraversableB and Traversable1B, in that, afaics, both have exactly the same instances. If Apply were a super-class of Applicative, we'd just require Apply for the type of the effect instead (the generic instances would just be a bit more complex to write, I think)

jcpetruzza commented 2 years ago

On further thought, Traversable1B has fewer instances than TraversableB, I think. One can't get an instance for:

data Unit (f :: k -> Type) = Unit

or, more in general, for any type with constructors where f does not occur.

masaeedu commented 2 years ago

@jcpetruzza Yes, exactly. This is why you would want Traversable1B to be a subclass of TraversableB.

If Apply were a super-class of Applicative, we'd just require Apply for the type of the effect instead (the generic instances would just be a bit more complex to write, I think)

Things are a bit confusing here because the relationship between Traversable/Traversable1B and Applicative/Apply is contravariant, so the variance is flipped around. Traversable is a superclass of Traversable1 as a consequence of Applicative being a subclass of Apply.

fumieval commented 2 years ago

FYI barbies-th provides an equivalent AccessorsB class: https://hackage.haskell.org/package/barbies-th-0.1.9/docs/Barbies-TH.html#t:AccessorsB

It would be great to have this in barbies so that barbies-th focus on the Template Haskell part

masaeedu commented 2 years ago

@fumieval FWIW i've revised my position on the "bag of lenses" class. I now believe that what we want is the class of HKDs that are products. A naive encoding of this is pretty straightforward:

type HKD k = (k -> Type) -> Type

type HList :: [k] -> HKD k
data HList xs f
  where
  HNil :: HList '[] f
  HCons :: f x -> HList xs f -> HList (x ': xs) f

type ProductB :: HKD k -> Constraint
class ProductB (b :: HKD k)
  where
  type Components b :: [k]
  to :: b f -> HList (Components b) f
  from :: HList (Components b) f

-- Laws:
-- to . from = id
-- from . to = id

I believe this would subsume ConstraintsB, and would also supply the bag of lenses we're looking for in certain circumstances. Additionally, it provides a good basis for discussing the duality of products and coproducts and their interactions.