Skip to content

-XApplyingVia — sort @(Int via Down Int) #218

Open
Icelandjack wants to merge 2 commits intoghc-proposals:masterfrom
Icelandjack:patch-6
Open

-XApplyingVia — sort @(Int via Down Int) #218
Icelandjack wants to merge 2 commits intoghc-proposals:masterfrom
Icelandjack:patch-6

Conversation

@Icelandjack
Copy link
Copy Markdown
Contributor

@Icelandjack Icelandjack commented Apr 3, 2019

Applying Via

This proposes a new language extension ApplyingVia to change the behaviour of types. TypeApplications allows applying terms to an "at type"; applying via allows specifying a via type which is representationally equal to the at type. The new behaviour is inherited from instances of the via type.

This will allow sort to sort "down"

>> sort @(via Down) [1..5]
[5,4,3,2,1]

We discard the term-level Down and only use its type-level information just as ala. Applying via @(via f) an argument f :: proxy <at> <via> (usually proxy ~ (->)) means we get behaviour from the <via> type but end up at the <at> type. A more explicit syntax; @(<at> via <via>).

Down in sort @(via Down) is a term-level proxy of type Down :: a -> Down a and has the same meaning as sort @(forall a. a via Down a) (haven't figured the quantification out).

for_ loops which are usually left-to-right can iterate via Reverse, right-to-left

>> for_ [1..5] print
1
2
3
4
5
>> for_ @(via Reverse) [1..5] print
5
4
3
2
1

These use the same IO effect with sequential behaviour. For concurrent behaviour we can apply via Concurrently:

>> for_ @(via Reverse) @(via Concurrently) [1..5] print
5
4
321

This allows defining standard combinators:

($) :: (a -> b) -> (a -> b)
($) = (<$>) @(via Identity)

(&) :: a -> (a -> b) -> b
(&) = (<&>) @(via Identity)

sum, product :: Foldable f => Num a => f a -> a
sum     = fold @_ @(via Sum)
product = fold @_ @(via Product)

any, all :: Foldable f => (a -> Bool) -> (f a -> Bool)
any = foldMap @_ @(via Any)
all = foldMap @_ @(via All)

find :: Foldable f => (a -> Bool) -> (f a -> Maybe a)
find pred = foldMap @_ @(via First) \x -> [ x | pred x ]

Rather than

foldl :: Foldable f => (b -> a -> b) -> (b -> f a -> b)
foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z

bifoldl :: Bifoldable bi => (c -> a -> c) -> (c -> b -> c) -> (c -> bi a b -> c)
bifoldl f g z t = appEndo (getDual (bifoldMap (Dual . Endo . flip f) (Dual . Endo . flip g) t)) z

-- This will not compile without a type signature, difficult to write in ghci
bifoldMapDefault :: forall bi m a b. (Bitraversable bi, Monoid m) => (a -> m) -> (b -> m) -> (bi a b -> m)
bifoldMapDefault = coerce (bitraverse :: (a -> Const m ()) -> (b -> Const m ()) -> (bi a b -> Const m (t () ())))

we define

foldl :: Foldable f => (b -> a -> b) -> (b -> f a -> b)
foldl = flip . foldMap @_ @(via Dual . Endo) . flip
foldl = flip . foldMap @​(via Reverse) @(via Endo) . flip

bifoldl :: Bifoldable bi => (c -> a -> c) -> (c -> b -> c) -> (c -> bi a b -> c)
bifoldl f g = flip (bifoldMap @_ @(via Dual . Endo) (flip f) (flip g))
bifoldl f g = flip (bifoldMap @(via BiReverse) @(via Endo) (flip f) (flip g))

-- correct type is inferred
bifoldMapDefault :: Bitraversable bi => (a -> m) -> (b -> m) -> (bi a b -> m)
bifoldMapDefault = bitraverse @_ @(via Const)

@Icelandjack
Copy link
Copy Markdown
Contributor Author

This replaces some use cases for ala

@luqui
Copy link
Copy Markdown

luqui commented Apr 3, 2019

Cool, I like this! via is showing us a new style of programming by composing algebraic structures.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 3, 2019

Thanks man, I would love to find more examples compositions; here is one I found on IRC

>> (($ "") . appEndo . getConst . traverse (\x -> Const (Endo (x:)))) "hello"
"hello"
>> traverse @_ @(String -> String via Const (Endo String) _) (:) "hello" ""
"hello"
>> traverse @_ @(via Const . Endo) (:) "hello" ""

@gridaphobe
Copy link
Copy Markdown
Contributor

Would this allow us to break the invariants guaranteed by global instance coherence? For example, would I be allowed to write the following?

s :: Set Int
insert @(Int via Down Int) s 5

It seems that your suggestion is to desugar ViaApplications to applications of coerce, so presumably this would desugar to something like:

coerce (insert @(Down Int)) s 5

I would hope that the work on roles and safe coercions would save us here, and indeed it does.

ghci> coerce @(Down Int -> Set (Down Int) -> Set (Down Int)) @(Int -> Set Int -> Set Int) insert s 5
<interactive>:1:1: error:
     Couldn't match type Down Int with Int
        arising from a use of coerce
     In the expression:
        coerce
          @(Down Int -> Set (Down Int) -> Set (Down Int))
          @(Int -> Set Int -> Set Int)
          insert
          s
          5

The proposal should address this potential issue, alongside other fleshing out (like specifying what the desugaring plan is :)

@nomeata
Copy link
Copy Markdown
Contributor

nomeata commented Apr 4, 2019

Interesting idea, worthwhile exploring!

(shameless plug) reminds me of https://www.reddit.com/r/haskell/comments/765ogm/multiple_type_class_instances_for_the_same_type/doc8enp/

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Vote for names: -XApplyingVia

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Vote for names: -XViaApplications

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Given Fold this allows us to specify the existential Monoid independently (apart from representation) from the type we want to provide

data Fold :: Type -> Type -> Type where
 Fold :: Monoid mm
      => { tally     ::  a -> mm
         , summarize :: mm ->  b
         }
      -> Fold a b
>> :t Fold 
.. :: Monoid mm => (a -> mm) -> (mm -> b) -> Fold a b

We might want our Monoid to be Endo _ (as in fromRightFold) or Dual (Endo _) as in fromLeftFold

>> :t Fold @(Endo _)
.. :: (a -> Endo _) -> (Endo _ -> b) -> Fold a b
>> :t Fold @(Dual (Endo _))
.. :: (a -> Dual (Endo _)) -> (Dual (Endo _) -> b) -> Fold a b
>> :t \@x -> Fold @(x->x via Endo x)
.. :: (a -> (x -> x)) -> ((x -> x) -> b) -> Fold a b
>> :t \@x -> Fold @(x->x via Dual (Endo x))
.. :: (a -> (x -> x)) -> ((x -> x) -> b) -> Fold a b

@Icelandjack Icelandjack changed the title "ViaApplications" -- fold @_ @(Int via Product a) -XApplyingVia — sort @(Int via Down Int) Apr 4, 2019
@iamrecursion
Copy link
Copy Markdown

I like this idea, and think it's definitely worth exploring further here! The fact that via is starting to become a way of expressing relationships that can't otherwise be inferred is quite cool.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 4, 2019

@iamrecursion Thank you! It is exciting

@gridaphobe This is based on coerce like -XDerivingVia and inherits safety guarantees from the role system so it shouldn't introduce any issues (as demonstrated, Set's nominal argument prevents it from being coerced; all is well!)

It works with Data.List.insert as [a] is representational in a

>> insert @(Int via Down Int) 0 [1..4]
[1,2,3,4,0]
>>
>> (coerce $ insert @(Down Int) :: Int -> [Int] -> [Int]) 0 [1..4]
[1,2,3,4,0]

funky 🤐 I would appreciate help establishing a clear semantics and desugaring

@nomeata Thanks, it's all built on your work on Coercible! Biggest part of via is informing Coercible what parameters should be left alone. That's exactly what your "shameless plug" *With functions do manually, they match up the type variables but it requires a lot of annotations on "both ends" of coerce @((c -> a) -> ([c] -> a)) @((c -> b) -> [c] -> b) when the only change is a to b. I aim to capture that with @(b via a)

@Jashweii
Copy link
Copy Markdown

Jashweii commented Apr 4, 2019

So the idea is that

fun :: forall a. F(a)
fun @(X via Y)

is instantiated to F(Y) and then coerced to F(X), correct? What does this do if some constraints depend on and aren't satisfied by Y? Would they remain as constraints on Y?

example :: forall s m . (MonadState s m, IsList s, Ord s) => ...
example' = example @(Int via Down Int)

should example' be

forall m . (MonadState (Down Int) m, IsList (Down Int), Ord (Down Int)) => ...
-- Ord (Down Int) is solved, of course, and in the general case Ord (Down a) would become (Ord a)

Edit: If this is the behaviour it would be something more like:

fun :: forall a. C(a) => F(a)
fun @(X via Y) :: C(Y) => F(X)

@DanBurton
Copy link
Copy Markdown

I'm a little uneasy with it.

Deriving via introduced codegen in a context where we were already doing codegen (the deriving mechanism). Applying via introduces codegen in a place where it was not found previously (type application).

I think instead of type coercions, what I actually want to see is something like explicit typeclass dictionary applications. So if the function calls for (Ord a) I want to be able to say "sort @(Ord a via Down a)". (Insert much handwaving here about how to tell which "a" is intended if there are multiple Ord constraints). I am more able to wrap my head around what this does, because it's just choosing a different typeclass dictionary to pass to the function.

Of course this is a Bad Idea to do with Data.Set functions. Perhaps that detail should be addressed in this proposal as well?

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 4, 2019

Why do you call this codegen @DanBurton?

Coercing Set is ruled out from the get-go, I do think this (type-based approach) is the right point in the design space since instance resolution is type-directed but I need a more precise description of what you have in mind before I say more

@goldfirere
Copy link
Copy Markdown
Contributor

Despite its being extraordinarily useful, I've never loved deriving-via, essentially for the reasons @DanBurton describes. The key trick in deriving-via is to use newtypes to stand in for named instances. Clever use of coerce makes this work. And work it does. However, I find it indirect.

A case in point is a recent usage of deriving-via: A class I wanted instances of didn't have the right generic default for my purposes. So I wrote a newtype, an instance for that newtype, and then used a lot of deriving-via. But I'd much rather have done this without the newtype (which wasn't otherwise useful): if I could name the instance directly, then this would be simpler. One could argue that the newtype acts as the name of an instance, and that one would be right -- but again, let's not have play-acting: let's just name the instance.

This proposal (which I think holds water and may prove to be just as useful in practice as deriving-via) expands our use of newtypes as proxies for named instances. As an alternative, I'd love to consider Coherent Explicit Dictionary Application for Haskell by Thomas Winant and Dominique Devriese. I liked the presentation at last year's Haskell Symposium, but I haven't studied this in detail. Would it lead to a cleaner way to do all this?

@DanBurton
Copy link
Copy Markdown

I call it "codegen" because it is basically a macro to insert "coerce" at certain points in the code. Coerce might have 0 runtime cost, but it is still "code" with a semantic meaning.

Reflecting on this, I should state one further cause for unease, which is that type applications have historically served only to "narrow" the type of a function call. Erasing the type application will either a) be inferred to the same type, or b) be too ambiguous to infer. ApplyingVia allows type applications to change the type of a function call, so that erasing the type application c) changes the behavior of the code.

It may be worthwhile to do, but it is something that type applications have not been able to do before. If turning on this extension means that type applications cannot be "safely erased", then this is worth noting. (Define "safely erased" to mean "either a) the code runs the same, or b) it fails to compile"; the latter being a more tedious case to deal with, but still "safe" in the sense that you don't end up with different runtime code.)

@DanBurton
Copy link
Copy Markdown

DanBurton commented Apr 4, 2019

I do think this (type-based approach) is the right point in the design space since instance resolution is type-directed but I need a more precise description of what you have in mind before I say more

I don't have a precise definition in mind, I just have a few reference points.

  • Something comparable to Scala's explicit typeclass applications. (Not saying this is an ideal point in the design space, but it's worth discussing.)
  • In GHC core, instance dicts are just named values and can be referred to by name
  • As @goldfirere said, "let's just name the instance". I'm imagining things like
    • deriving instance named SumInt as Monoid Int via Sum Int
    • instance named Blah as Monoid Int where ...
    • mconcat @{SumInt} or some other unique new syntax for instance applications. This is probably the point you want to debate but I don't yet have a clear idea of what this should be. I apologize for not giving you something concrete to debate against.

So what I'm saying is, if you're going to make a case for ApplyingVia, then there should be some attempt at comparing/contrasting the other points in the design space for similar features. In this case, that would be Coherent Explicit Dictionary Application for Haskell, as mentioned by @goldfirere (which I have not yet reviewed but it sounds like exactly what I'm picturing).

I'd also love to see a more explicit comparison/contrast to ala & similar term-level tools, which are already mentioned briefly in the proposal. ApplyingVia is certainly more flexible, in theory.

@AntC2
Copy link
Copy Markdown
Contributor

AntC2 commented Apr 5, 2019

foldMap @f @(Bool via Any) @a
...
fold @[] @(a via Sum a)

This is presumably part of the continuing campaign to make Haskell indistinguishable from line noise. Please stop with the hier@glyphics already.

What's become of the succinct yet readable, elegant language by which I came to love functional programming?

Is this intended merely as a shorthand for something I could write today? From the proposal's "(handwave)" I'm not seeing what; so I can't see what all those @s are intended to represent, nor if there's much saving of code.

Is there any benefit for users of packages as opposed to writers of packages? Will users get faced with these hier@glyphics (perhaps in error messages)? I'm experiencing that GHC today with all its extensions is just too big of a language to be usable. The language is in danger of disappearing up its categorial fundament.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 5, 2019

I'll make time to address comments about names and what that entails later.

@AntC2: Thanks for sharing your opinions. The notation fits my view of Haskell, @ existed long before and the often referenced "Coherent Explicit Dictionary Application" proposes a similar notation @{downOrd}, @{downOrd as Ord a}.

Is this intended merely as a shorthand for something I could write today?

Yes a shorthand for coerce like -XDerivingVia

Not limited to library authors. Doesn't appear in error messages.

@klapaucius
Copy link
Copy Markdown

succinct ..., elegant language

(($ []) . appEndo . getConst . traverse (\x -> Const (Endo (x:)))) "hello"

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 5, 2019

I will describe Coherent Explicit Dictionary Application (CEDA from here on) and its approach. It's a good paper and these are my preliminary observations.

There is an obvious appeal of exposing dictionaries to users as CEDA does, a case-insensitive nub can be written with nub @{ Eq.Dict { (==) = \str str' -> map toLower str == map toLower str' }. To be clear via is more verbose and can't straight-forwardly match this example until we get dependent types.

What does CEDA entail?

We generate a .. -> Type per .. -> Constraint, the paper introduces single-method Eq and Ord classes which generate dictionary records suffixed with .Dict:

newtype Eq.Dict a = Eq.Dict { (==) :: a -> a -> Bool }

data Ord.Dict a = Ord.Dict { parent1 :: Eq.Dict a, (<=) :: a -> a -> Bool }

fields named parent{1,2,..} store superclasses in the order they appear in the type declaration. Now order matters, so is this a breaking change

class (Num a, Ord a) => Real a ..
-- to
class (Ord a, Num a) => Real a ..

(I assume the order in which methods are declared matters in the same way, this would be nice with row types!) The design of CEDA does not support associated types and MINIMAL pragmas and default methods are ignored, so the dictionary records of Eq and Ord from base require more than the minimal fields defined in the paper

data Eq.Dict a = Eq.Dict { (==) :: a -> a -> Bool, (/=) :: a -> a -> Bool }

data Ord.Dict a = Ord.Dict { parent1 :: Eq.Dict a, compare :: a -> a -> Ordering, (<) :: a -> a -> Bool, (<=) :: a -> a -> Bool, (>) :: a -> a -> Bool, (>=) :: a -> a -> Bool, max :: a -> a -> a, min :: a -> a -> a }

CEDA recommends writing smart constructors, and case-insensitive nub becomes(?)

smartEq :: (a -> a -> Bool) -> Eq.Dict a
smartEq (===) = Eq.Dict { (==) = (===), (/=) = \a b -> not (a === b) }

nub @{ smartEq (\str str' -> map toLower str == map toLower str') }

They optionally propose a special constraint to get the dictionary for a given type, whose instances are generated by the compiler

class HasDict (c :: Constraint) where
  type DictOf c :: Type
  getDict :: c => DictOf c

to avoid writing

Ord.Dict 
  { parent1 = Eq.Dict
    { (==) = (==)
    , (/=) = (/=)
    }
  , compare = compare
  , (<)     = (<)
  , (<=)    = (<=)
  , (>)     = (>)
  , (>=)    = (>=)
  , max     = max
  , min     = min
  }

If we have a function with two constraints of the same type class

foo :: (Eq a, Eq b) => (a, b) -> (a, b) -> ..

how do we override only Eq a or the other Eq b? Now the variable names in the function's type signature start to matter as CEDA introduces the notation foo @{eq1 as Eq a} or foo @{eq2 as Eq b} to refer explicitly to these unordered constraints. Alpha-renaming the type of foo to .. => (b, a) -> (b, a) -> .. is now a breaking change.

That is a big proposal with many choices, it requires compiler generated dictionaries and optional compiler generated constraint where superclass, method order and alpha-equivalent types start to matter.


via isn't as exciting and takes from its surroundings (instances, newtypes and coerce). As a result MINIMAL pragmas, default methods and associated type classes all work like normal Haskell (as it is) so we only need to specify (==) or (/=).

Because it is type-directed rather than dictionary-directed we never need to disambiguate between constraints (we do need a type signature for foo just as CEDA (which is interesting) to allow -XTypeApplications)

foo :: forall a b. (Eq a, Eq b) => (a, b) -> (a, b) -> ..

since type variables are already ordered: foo @(a via Eq1 a) @(b via Eq2 b) means the same as foo @(b via Eq1 b) @(a via Eq2 a) (unlike CEDA the order is not dependent on the name of the type variables a, b as they appear in the type signature of foo).

There are some other questions but I'm tired so I won't answer them right now! Idk not really a conclusion

@goldfirere
Copy link
Copy Markdown
Contributor

goldfirere commented Apr 6, 2019

As I'm thinking about this more, I'm not sure what the exact specification of this would be. I thought we would treat f @(T1 via T2) as coerce @(type of f @T2) @(type of f @T1) f, but that's not quite right. For example, if we have sort :: forall a. Ord a => [a] -> [a], then sort @(Int via Down Int) is like coerce @(Ord [Down Int] => [Down Int] -> [Down Int]) @(Ord Int => [Int] -> [Int]) sort, but that's ill-typed, because Ord's role is nominal (like just about all classes).

So then we can say that f @(T1 via T2) is really coerce @(maximally instantiated type of f @T2) @(maximally instantiated type of f @T1) f, where maximal instantiation instantiates all Inferred arguments (like class constraints). This means that sort @(Int via Down Int) becomes coerce @([Down Int] -> [Down Int]) @([Int -> Int]) sort, which is probably what you want.

But what if more specified variables intervene? Then the maximal instantiation trick doesn't work. And what if there are class constraints that mention multiple variables? It's not quite clear to me. So I think we might need a full specification before going much further...

(By the way, many thanks for the summary and analysis of CEDA w.r.t. this proposal. Very helpful!)

@Icelandjack
Copy link
Copy Markdown
Contributor Author

similarly we could write

newtype Const a b = Const a

instance Functor (Const a) where
  fmap :: (b -> b') -> (Const a b -> Const a b')
  fmap _ (Const a) = Const a

instance Monoid a => Applicative (Const a) where
  pure :: b -> Const a b
  pure _ = Const mempty

  (<*>) :: Const a (b -> b') -> Const a b -> Const a b'
  Const a <*> Const a' = Const (a <> a') 

as

instance Functor (Const a via a) where
  fmap :: (b -> b') -> (a -> a)
  fmap _ = id

instance Monoid a => Applicative (Const a via a) where
  pure :: b -> a
  pure _ = mempty

  (<*>) :: a -> a -> a
  (<*>) = (<>) 

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 7, 2019

An observation

nub     :: forall a.               Eq  a => [a] -> [a]
nubOrd  :: forall a.               Ord a => [a] -> [a] 
nubHash :: forall a. Hashable a => Eq  a => [a] -> [a]

CEDA is dictionary-based, we cannot change the underlying function to one with different constraints without updating the dictionary to agree. Now we are hoist by the necessity to construct and name dictionaries (maybe you want to vary them separately?), same wth liftM, liftA and fmap

nub     @{eqCaseInsensitive}
nubOrd  @{ordCaseInsensitive}
nubHash @{hashCaseInsensitive} @{eqCaseInsensitive}

liftM @{myMonad}
liftA @{parent1 myMonad}
fmap  @{parent1 (parent1 myMonad)}

The type-focused -XApplyingVia however does not change

nub     @(String via CaseInsensitive)
nubOrd  @(String via CaseInsensitive)
nubHash @(String via CaseInsensitive)

liftM @(m via My m)
liftA @(m via My m)
fmap  @(m via My m)

One could argue that the newtype acts as the name of an instance, and that one would be right -- but again, let's not have play-acting: let's just name the instance.

CEDA does not mean that a single name replaces the CaseInsensitive(..) export of required by via, instead it gets replaced by one name per instance

module CEDA (eqCaseInsensitive, ordCaseInsensitive, hashCaseInsensitive, ..)

@goldfirere
Copy link
Copy Markdown
Contributor

I'm not so worried about the covariance between function name and dictionary name in CEDA. We could always have a way of abstracting over that, by (say) having a class that classifies dictionary types, so the correct dictionary could be selected in a type-directed fashion.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 7, 2019

The show stopper for -XApplyingVia; while I can apply traverse at some monoid m via Const m to obtain foldMapDefault

   (\@m -> traverse @_ @(m via Const m))
:: Traversable t => Monoid m => (a -> m) -> (t a -> m)

What about via Identity to obtain fmapDefault?

   traverse @_ @(?? via Identity)
:: Traversable t => (a -> b) -> (t a -> t b)

If all uses of the Applicative functor f are fully saturated, we could maybe write

traverse @_ @(forall x. x via Identity x)

type Id a = a
traverse @_ @(Id via Identity)

type family Id a where Id a = a
traverse @_ @(Id via Identity)

traverse @_ @((\x -> x) via Identity)

but I'm not happy with those designs :) just putting it out there

@Icelandjack
Copy link
Copy Markdown
Contributor Author

This cool implementation of a SAT solver https://julesh.com/2016/09/22/abusing-the-continuation-monad/, the idea to replace \k -> k (k True) with \k -> k True || k False came from Sjoerd

sat :: Int -> ([Bool] -> Bool) -> Bool
sat n = runCont $ replicateM n $ cont $ \k -> k True || k False

Here it is in longer form with no runCont and cont

sat n = replicateM @(forall x. (x -> Bool) -> Bool via Cont Bool x) n $ \k -> k True || k False

type Cont_ res a = ((a -> res) -> res)
sat n = replicateM @(Cont_ Bool via Cont Bool) n $ \k -> k True || k False

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Apr 7, 2019

Keeping track of examples.

>> sequenceA @[] @[] ["ABC", "123", "!"]
["A1!","A2!","A3!","B1!","B2!","B3!","C1!","C2!","C3!"]
>> sequence @([] via ZipList) @[] ["ABC", "123", "!"]
["A1!","A2!","A3!","B1!","B2!","B3!","C1!","C2!","C3!"]
>> sequence @[] @([] via ZipList) ["ABC", "123", "!"]
["A1!"]
>> sequence @([] via ZipList) @([] via ZipList) ["ABC", "123", "!"]
["A1!"]

Accumulating sum (and Expr) and length

>> foldMap @[] @((Float, Int) via (Sum Float, Sum Int)) (,1) [pi, pi, pi]
(9.424778,3)

>> import Debug.SimpleReflect
>>
>> foldMap @[] @Expr realFrac [pi,pi,pi]
3.1415927410125732 <> 3.1415927410125732 <> 3.1415927410125732 <> mempty
>> foldMap @[] @(Expr via Dual Expr) realFrac [pi,pi,pi]
((mempty <> 3.1415927410125732) <> 3.1415927410125732) <> 3.1415927410125732
>> foldMap @[] @((Float,Expr,Int) via (Sum Float,Expr,Sum Int)) (\fl -> (fl, realToFrac fl,1) [pi, pi, pi]
(9.424778,3.1415927410125732 <> 3.1415927410125732 <> 3.1415927410125732 <> mempty,3)

Data.Fold.M

instance Folding M where
  run s (M k h m (z :: m)) = reify (m, z) $
    \ (_ :: Proxy s) -> k $ runN (foldMap (N #. h) s :: N m s)

-- becomes (and avoids reify with DH)

instance Folding M where
  run s (M k h m (z :: m)) = reify (m, z) $
    \ (_ :: Proxy s) -> k $ foldMap @_ @(m via N_ m s) h s
foldMapA :: forall t f a b. (Foldable t, Applicative f, Monoid b) => (a -> f b) -> (t a -> f b)
foldMapA f = getAp . foldMap (Ap . f)

-- becomes
foldMapA :: forall t f a b. (Foldable t, Applicative f, Monoid b) => (a -> f b) -> (t a -> f b)
foldMapA = foldMap @_ @(b via Ap f b)

bind from ad

bind :: forall f a. Traversable f => f a -> (f (Kahn a), (Int,Int))
bind as = (r,(0,hi)) where
  r  :: f (Kahn a)
  hi :: Int
  (r,hi) = runState (mapM freshVar as) 0

  freshVar :: a -> State Int (Kahn a)
  freshVar a = state $ \s -> let s' = s + 1 in s' `seq` (var a s, s')
-- 
bind :: forall f a. Traversable f => f a -> (f (Kahn a), (Int, Int))
bind as = (r,(0,hi)) where
  r  :: f (Kahn a)
  hi :: Int
  (r,hi) = mapM @_ @(Int -> (a, Int) via State Int a) 
    freshVar as 0

  freshVar :: a -> Int -> (Kahn a, Int)
  freshVar a s = let s' = s + 1 in s' `seq` (var a s, s')

mapAccum{L,R}

mapAccumL f s t = runStateL (traverse (StateL . flip f) t) s
mapAccumR f s t = runStateR (traverse (StateR . flip f) t) s

mapAccumL = flip . traverse' @(forall x. a -> (a, x) via StateL a x) . flip
mapAccumR = flip . traverse' @(forall x. a -> (a, x) via StateR a x) . flip

from irc, although I prefer the locality it shouldn't be this much longer

> replicateM 3 (StateT (reads :: ReadS Integer)) `runStateT` "12 13 14"
[([12,13,14],"")]
> replicateM @(String -> [(a, String)] via via StateT String [] a) 3 (reads @Integer) "12 13 14"
[([12,13,14],"")]

async

mapConcurrently :: Traversable t => (a -> IO b) -> t a -> IO (t b)
mapConcurrently = traverse @_ @(IO via Concurrently)

mapConcurrently_ :: Foldable f => (a -> IO b) -> f a -> IO ()
mapConcurrently_ = traverse_ @_ @(IO via Concurrently)

replicateConcurrently :: Int -> IO a -> IO [a]
replicateConcurrently = replicateM @(IO via Concurrently)

replicateConcurrently_ :: Int -> IO a -> IO ()
replicateConcurrently_ n = replicateM_ @(IO via Concurrently)

irc

>> :t getCompose .: traverse @[] @(Compose IO (Either _)) . fmap Compose
.. :: (a -> IO (Either err b)) -> [a] -> IO (Either err [b])
>> :t \@err -> traverse @[] @(forall x. IO (Either err x) via Compose IO (Either err) x)
.. :: (a -> IO (Either err b)) -> [a] -> IO (Either err [b])

Löb

löb :: Functor f => f (f a -> a) -> f a
löb funs = fix (\fs -> fmap ($ fs) funs)

löb @(forall r a. ((a -> r) -> r) via Cont r a)
:: (((((a -> r) -> r) -> a) -> r) -> r) -> ((a -> r) -> r)
-- better solution:
löb @(via cont)
:: (((((a -> r) -> r) -> a) -> r) -> r) -> ((a -> r) -> r)

I noticed (<*>) @ZipList and pure @ZipList in Eisenberg's thesis

Screenshot from 2019-04-18 17-02-58

Screenshot from 2019-04-18 17-03-05

So we wanted to use the functionality of ZipList while operating on [] (see reddit, blog).

lift :: forall f a b n. Applicative f => Applyable b n => (a -> b) -> (f a -> AppFunc n f b)

>> lift @[] (+) [1, 2] [10, 20]
[11,21,12,22]
>> lift @ZipList (+) (ZipList [1, 2]) (ZipList [10, 20])
ZipList {getZipList = [11,22]}

Due to the incompleteness we cannot apply it to @[] via @ZipList (due to Listify the type family)

• Could not deduce: Coercible (AppFunc n ZipList b) (AppFunc n [] b)

but we happen to know this holds right? well, if we did we should have the option of unsafe via or via!

gzipWith = lift @([] unsafe via ZipList)

memoize

memoizeFinite :: forall a. Enum a => Bounded a => (a -> b) -> (a -> b)
memoizeFinite = memoize @(a via Finite a)

A lot of cool impredicative examples, using experimental syntax

pure @(via! Yoneda    @Identity) :: a -> forall res. (a -> res) -> res
pure @(via! Codensity @Identity) :: a -> forall res. (a -> res) -> res

pure @(via! Yoneda    @((->) b)) :: a -> forall res. (a -> res) -> (b -> res)
pure @(via! Codensity @((->) b)) :: a -> forall res. (a -> (b -> res)) -> (b -> res)

fmap @(via! Yoneda        @Endo) :: (a -> b) -> ((forall x. (a -> x) -> (x -> x)) -> (forall y. (b -> y) -> (y -> y)))
fmap @(via! Ran @Identity @Endo) :: (a -> b) -> ((forall x. (a -> x) -> (x -> x)) -> (forall y. (b -> y) -> (y -> y)))
fmap @(via! Codensity     @Endo) :: (a -> b) -> ((forall x. (a -> (x -> x)) -> (x -> x)) -> (forall y. (b -> (y -> y)) -> (y -> y)))

from lens

beside l r f = tabulate $ getCompose #. bitraverse (Compose #. sieve (l f)) (Compose #. sieve (r f))

beside l r f = tabulate $ bitraverse @_ @(via Compose @(Rep q) @f) (sieve (l f)) (sieve (r f))

pad

pad :: String -> Int -> String
pad str n = str <|> @(via ZipList) replicate n ' '

QuickCheck:

>> sample $ arbitrary @(via ASCIIString)
""
"^"
"U_\FS"
"qG\DC2F~"
"hR]=6"
"\US\ETXL\DLE%aK E"
" \DLE,pDK\NUL@~\EOT5$"
")\fTB\DC3)\RSD%_UDbH"
"\SI\v K\\"
"P"
"\GS+'f1^Uu<"

>> sample $ arbitrary @([[Int]] via OrderedList (NonEmptyList (Positive (Large Int))))
[]
[[1]]
[[5,8,6,5],[6,6,8]]
[[1,5],[1,15,15,14],[7,10,3,2,16],[10,13,5,11],[11,13,9,16],[16,10,4,8]]
[[8,62,7,26,20,14],[35,41,25,56,28],[77,80,109,107,103,42],[78,78,70,125,104,36]]
..
newtype ArbitraryEnum       a = ArbitraryEnum       a deriving newtype (Bounded, Enum)
newtype ArbitraryFractional a = ArbitraryFractional a deriving newtype (Num, Fractional)

instance (Bounded a, Enum a) => Arbitrary (ArbitraryEnum a) where
  arbitrary :: Gen (ArbitraryEnum a)
  arbitrary = arbitraryBoundedEnum

instance Fractional a => Arbitrary (ArbitraryFractional a) where
  arbitrary :: Gen (ArbitraryFractional a)
  arbitrary = arbitrarySizedFractional
  
>> data B = F | T deriving stock (Show, Bounded, Enum)
>>
>> sample $ arbitrary @(via ArbitraryEnum)
T
F
..

>> newtype FLOAT = FLOAT Float deriving stock (Show) deriving newtype (Num, Fractional)
>>
>> sample $ arbitrary @(via ArbitraryFractional)
FLOAT 0.0
FLOAT (-2.2061577)
FLOAT 13.428107
FLOAT 3.4943657
FLOAT (-9.44709)
FLOAT 1.1487609
FLOAT 12.563746
FLOAT 4.5106926
FLOAT (-10.366447)
FLOAT (-16.974293)
FLOAT 26.306278

Control.Applicative.Backwards

>> import Control.Applicative.Backwards
>>
>> sequenceA [Left 1, Left 2]
Left 1
>> sequenceA @_ @(via Backwards) [Left 1, Left 2]
Left 2
newtype Op cat a b = Op { getOp :: cat b a }

instance Category cat => Category (via getOp) where
  id :: cat a a
  id = id

  (.) :: cat b c -> cat a b -> cat a c
  (.) = flip (.)

(>>>) :: Category cat => cat a b -> cat b c -> cat a c
(>>>) = (.) @(via Op)

Cute :]

($) :: (a -> b) -> (a -> b)
($) = (<$>)    @(via Identity)
    = (<*>)    @(via Identity)
    = (=<<)    @(via Identity)
    = traverse @(via Identity)  @(via Identity)
    = (=<<<)   @(via IdentityT) @(via Identity)

(&) :: a -> (a -> b) -> b
(&) = (<&>)  @(via Identity)
    = (<**>) @(via Identity)
    = (>>=)  @(via Identity)
    = for    @(via Identity)  @(via Identity)
    = (>>>=) @(via IdentityT) @(via Identity)

id :: a -> a
id = pure      @(via Identity)
   = join      @(via Identity)
   = (??)      @(via Identity)
   = fmap      @(via Const)     not
   = contramap @(via Const)     (+ 1)
   = lift      @(via IdentityT) @(via Identity)
   = (() <$ @(via Const))
   = ($> @(via Const) ())

const :: a -> b -> a
const = (<$) @(via Identity)
      = ($>) @(via Const)
      = ($<) @(via Const)

flip const :: b -> a -> a
flip const = ($>) @(via Identity)
           = (<$) @(via Const)
           = (>$) @(via Const)

mempty :: Monoid m => m
mempty = pure     @(via Const) ()
       = fold     @(via Const) ()
       = traverse @(via Const) not "abc"

mappend :: Monoid m => m -> m -> m
mappend = (<*>) @(via Const)

fmap :: (a -> b) -> Functor f => (f a -> f b)
fmap = collect @(via Identity)

test :: (a -> Bool) -> (a -> Maybe a)
test = find @(via Identity)

filter :: (a -> Bool) -> ([a] -> [a])
filter = filterM @(via Identity)

fix :: (a -> a) -> a
fix = mfix @(via Identity)

cycle :: Monoid m => m -> m
cycle = forever @(via Const)

replicate :: Int -> a -> [a]
replicate = replicateM @(via Identity)

mtimes :: Int -> Monoid m => m -> m
mtimes = replicateM  @(via Const)
       = replicateM_ @(via Const)

(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
(>=>) = (>>>) @(via Kleisli)

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
(<=<) = (<<<) @(via Kleisli)

(\case True -> id; False -> const mempty) :: Bool -> Monoid m => m -> m
(\case True -> id; False -> const mempty) = when @(via Const)

(\case True -> const mempty; False -> id) :: Bool -> Monoid m => m -> m
(\case True -> const mempty; False -> id) = unless @(via Const)

foldMap :: Monoid m => (a -> m) -> ([a] -> m)
foldMap = filterM @(via Const)

   ifoldMap @_ @(via Identity)
:: Monoid m => (() -> a -> m) -> (a -> m)

example from Justin Le, here the via proxy gets too complicated; what to do

-- changes underlying state, like zoom from lens

overStateT :: Functor m => (t -> s) -> (s -> t) -> (StateT s m a -> StateT t m a)
overStateT = dimap @(via Star . (Compose .) . runStateT)

transpose = sequenceA on ZipList

transpose :: [[a]] -> [[a]]
transpose = sequenceA @[] @(via ZipList)

All Sorts of Permutations

-- like we have
>> filterM @[] (const [False, True]) [1..3]
[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]

-- has anyone used it with ZipList
>> filterM @(via ZipList) (\a -> ['1' `elem` show a, even a, odd a]) [1..10]
[[1,10],[2,4,6,8,10],[1,3,5,7,9]]
>> guard @[] True
[()]
>> guard @[] False
[]

>> guard @(via ZipList) True
repeat ()
>> guard @(via ZipList) False
[]
>> replicateM @(via ZipList) 10 "Earth"
["EEEEEEEEEE","aaaaaaaaaa","rrrrrrrrrr","tttttttttt","hhhhhhhhhh"]

>> zipWithM (via ZipList) (\a b -> [take a b, drop a b]) [1..3] ["ABC", "abc", "!?#"]
[["A","ab","!?#"],["BC","c",""]]

Function instance for numbers, via Ap

>> (negate + @(via Ap) negate) 10
-20

At this point I would favour the original notation, there may be a better notation

>> mconcat @([Int] via ZipList `Ap` Sum Int) [ [1..4], [10,20,30], [100,200] ]
[111, 222]
>> mconcat @(via Ap . ZipList . map Sum) [ [1..4], [10,20,30], [100,200] ]
[111, 222]

>> mconcat @([Int] via [] `Ap` Sum Int) [[1,2,3,4], [10,20,30], [100,200]]
[111,211,121,221,131,231,112,212,122,222,132,232,113,213,123,223,133,233,114,214,124,224,134,234]
>> mconcat @(via Ap . map Sum) [[1,2,3,4], [10,20,30], [100,200]]
[111,211,121,221,131,231,112,212,122,222,132,232,113,213,123,223,133,233,114,214,124,224,134,234]

>> optional @[] "hello"
[Just 'h',Just 'e',Just 'l',Just 'l',Just 'o',Nothing]
>> optional @(via ZipList) "hello"
[Just 'h',Just 'e',Just 'l',Just 'l',Just 'o',Nothing,Nothing,Nothing,Nothing,No

asum :: Foldable t => Alternative f => t (f a) -> f a
asum = mconcat @(via Alt)

>> asum @[] @ZipList (words "H -e --l ---l ----o -----!")
"Hello!"

>> mconcat @(via Alt . ZipList) (words "H -e --l ---l ----o -----!")
"Hello!"

>> :set +m
>> let
>| (<·>) :: Applicative f => [f a] -> [f a] -> [f a]
>| (<·>) = (<|>) @(via Compose @ZipList)
>|
>> words "HELLO" <·> words "- my" <·> words "- - BEST" <·> words "- - - !"
["HELLO","my","BEST","!"]
reverse :: [a] -> [a]
reverse = foldMap @[] @(via Dual) pure
        = toList @(via Reverse)

Reverse

-- Find from end-to-front
>> find @(via Reverse) even [100, 1,3,5,7,9, 2]
Just 2

-- Print in reverse
>> for_ @(via Reverse) [1..5] print
5
4
3
2
1

-- Print in reverse, concurrently
>> for_ @(via Reverse) @(via Concurrently) [1..5] print
5
4
3
21

Alternative Concurrently makes computations race concurrently:

-- if we omit $! the thunk of `product [1..30000]` would win without any work
>> asum @(via Concurrently) [pure $! product [1..30000], pure $! 2, pure $! product [1..30000] ]
2
> asum @[] @(via ZipList) $ words "g -e --t -u p"
"get"
> asum @(via Reverse) @(via ZipList) $ words "g -e --t -u p"
"put"

eq (Leibniz equality)

newtype a:=b = Refl { subst :: forall ctx. ctx a -> ctx b }

castWith :: (a := b) -> (a -> b)
castWith (Refl eq) = eq @(via Identity)

symm :: (a := b) -> (b := a)
symm (Refl eq) = eq @(via Data.Bifunctor.Flip.Flip)

newtype Lift f a a' = Lift (f a := f a')
lift :: (a := a') -> (f a := f a')
lift (Ref eq) = eq @(via Lift) id

runAp_ from free requires unsafe deriving to coerce past polymorphic functions (_ ~> _)

runAp_ :: Monoid m => (forall a. f a -> m) -> Ap f b -> m
runAp_ = runAp @(via! Const)
maximum = minimum @_ @(via Down)
-- or
minimum = maximum @_ @(via Down)

class Eq a => Ord a where
 ..
 min, max :: a -> a -> a
 min = max @(via Down)
 max = min @(via Down)

Showing "abc" as ['a','b','c'] (see How is the instance of Show for String written?)

>> print [1..3]
[1,2,3]
>> print ['a','b','c']
"abc"

newtype Lit = MkLit { unLit :: Char }

instance Show Lit where
  showsPrec :: Int -> Lit -> ShowS
  showsPrec = showsPrec @(via unLit)

  -- default 'showList :: [Lit] -> ShowS' implementation

>> print @(String via [Lit]) ['a','b','c']
['a','b','c']
>> print @(via map MkLit) ['a','b','c']
['a','b','c']
>> print @(via map Lisp) [1..8]
'(1 2 3 4 5 6 7 8)

>> print @(forall a. [[a]] -> [Lisp [Lisp a]]) [[1],[2],[3],[4]]
'('(1) '(2) '(3) '(4))
>> print @(via lispify) [[1],[2],[3],[4]]
'('(1) '(2) '(3) '(4))

newtype Lisp a = Lisp { unLisp :: a }

instance Show a => Show (Lisp a) where
 showsPrec :: Int -> Lisp a -> ShowS
 showsPrec = showsPrec @(via unLisp)

 showList :: [Lisp a] -> ShowS
 showList []     end = "'()" ++ end
 showList (x:xs) end = '\'' : '(' : shows x (showl xs) where

  showl :: [Lisp a] -> String
  showl []     = ')' : end
  showl (a:as) = ' ' : shows a (showl as)

type Via = Coercion
pattern Via = Coercion

lispify :: [[a]] `Via` [Lisp [Lisp a]]
lispify = Via

laws of Traversable:

  • Identity

    traverse Identity = Identity
    
    sequenceA . fmap Identity = Identity
  • Composition

    traverse (Compose . fmap g . f) = Compose . fmap (traverse g) . traverse f
    
    sequenceA . fmap Compose = Compose . fmap sequenceA . sequenceA

can be formulated in terms of via and via!

  • Identity
     traverse @(via Identity) id = id
     
     sequenceA @_ @(via! Identity) = id
  • Composition
    traverse @_ @(via Compose) (fmap g . f) = fmap (traverse g) . traverse f
    
    sequenceA @_ @(via! Compose) = fmap sequenceA . sequenceA

Data.Profunctor.Traversing

newtype Baz t b a = Baz { runBaz :: forall f. Applicative f => (a -> f b) -> f t }

sold :: Baz t a a -> t
sold (Baz baz) = baz @(via Identity) id

instance Traversable (Baz t b) where
 traverse :: forall f a a'. Applicative f => (a -> f a') -> (Baz t b a  -> f (Baz t b a'))
 traverse f (Baz baz) = bizarre <$> baz @(via Compose) (fmap sell . f) where

  bizarre :: Bazaar a' b t -> Baz t b a'
  bizarre = coerce

instance Traversing (->) where
 wander :: forall a b s t. (forall f. Applicative f => (a -> f b) -> (s -> f t)) -> (a -> b) -> (s -> t)
 wander traverse = traverse @(via! Identity)

-- Note that we are using via from both sides, we go from
--
--  a -> WrappedMonad m b
--  a -> m b
--  Kleisli m a b
instance Monad m => Traversing (runKleisli @m) where
 wander :: forall a b s t. (forall f. Applicative f => (a -> f b) -> (s -> f t)) -> ((a -> m b) -> (s -> m t))
 wander traverse = traverse @(via! WrappedMonad)

instance Profunctor p => Traversing (via runCofreeTraversing @pro) where
  traverse' p = p @(via! Compose)

we can also rewrite the laws

dimap Compose getCompose . traverse'  traverse' . traverse'

dimap Identity runIdentity . traverse'  id

with via!

traverse' @(via! Compose)  traverse' . traverse'

traverse' @(via! Identity)  id

Data.Functor.Rep

liftR2 :: Representable f => (a -> b -> c) -> (f a -> f b -> f c)
liftR3 :: Representable f => (a -> b -> c -> d) -> (f a -> f b -> f c -> f d)
liftR2 f fa fb    = tabulate $ \i -> f (index fa i) (index fb i)
liftR3 f fa fb fc = tabulate $ \i -> f (index fa i) (index fb i) (index fc i)

-- becomes
liftR2 = liftA2 @(via Co)
liftR3 = liftA3 @(via Co)

Newly added, Functor1 and Applied a f = f a

map1                :: Functor1 ho => (f ~> f') -> (ho f -> ho f')
map1 @(via Applied) ::             => (f ~> f') ->    (f ~> f')

fmapRep :: forall f a b. Representable f => (a -> b) -> (f a -> f b)
fmapRep f = cotraverse1 @_ @(via Applied) (f . runIdentity)

cotraverseRep :: Representable r => Functor f => (f a -> b) -> (f (r a) -> r b)
cotraverseRep f = cotraverse1 @_ @(via Composed) (f . fmap runIdentity)

distributeNap :: Representable f => Functor w => w (f a) -> f (w a)
distributeNap = cotraverse1 @_ @(via Composed) (fmap runIdentity)

Laws are fruitful:

distribute1 . Applied  fmap (Applied . Identity)

Functors representational in their first argument could be written

distribute1 @_ @(via Applied) = fmap Identity

or relating to iddist1 @_ @(via Applied) @(via Identity) given a new distribute1

dist1 :: forall f ho identity. Identity ~ identity => Representative f => Functor1 ho => ho f -> f (ho identity)
dist1 = distribute1
waitPrint :: Int -> IO ()
waitPrint n = do
  threadDelay n
  print n

microsecs :: [Int]
microsecs = [9000,2000,3000,4000,5000,8000]

>> foldMap waitPrint microsecs
9000
2000
3000
4000
5000
8000
>> foldMap @_ @(via Concurrently) waitPrint microsecs
2000
3000
4000
5000
9080000
0
>> foldMap @_ @(via Alt . Concurrently) waitPrint microsecs -- this uses 'race'! 2000 milliseconds runs first
2000

@DanBurton
Copy link
Copy Markdown

sat n = replicateM @(forall x. (x -> Bool) -> Bool via Cont Bool x) n $ \k -> k True || k False

Is there a way to leverage inference so that this doesn't require such verbosity (or a type alias)? So that you can write something like:

@(_ via Cont Bool)

What if the type application and the via application could be written side-by-side instead?

@_@(via Cont Bool)

And then the type could be omitted

@(via Cont Bool)

@DanBurton
Copy link
Copy Markdown

One reason I'm inclined to nudge the proposal in this direction is that it may help distinguish Via Applications as a separate sort of thing, rather than being a new special case in the "type" space.

instance Monoid a => Applicative (Const a)@(via a) where

@Icelandjack
Copy link
Copy Markdown
Contributor Author

@DanBurton

Is there a way to leverage inference so that this doesn't require such verbosity (or a type alias)?

The information we want to convey is forall a r. we are applying to (a -> r) -> r via Cont r a. Ignoring that (a -> r) -> r is not matchable in a, there is a place that gives exactly that information: The function cont

cont :: forall a r. ((a -> r) -> r) -> Cont r a

The way ala works is

[..] the first parameter is completely ignored on the value level, meaning the only reason you pass in the constructor is to provide type information.

We can do the same where @(via f) means applying to the domain of f via the codomain of f, but discarding the term f

replicateM @(Cont Bool)         :: Int -> Cont Bool a           -> Cont Bool [a]
replicateM @(via cont @_ @Bool) :: Int -> ((a -> Bool) -> Bool) -> (([a] -> Bool) -> Bool)

The sat example becomes (@Bool is inferred), I think this works for most of the examples and tends to be as short as I can reasonably expect

sat :: Int -> ([Bool] -> Bool) -> Bool
sat n = replicateM @(via cont) n (\k -> k True || k False)

Similarly it could work for any "proxy" f :: proxy ty viaTy

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented May 4, 2019

Using the same value-level witness for instance declarations, it would probably be the other way round

runContT :: ContT r m a -> ((a -> m r) -> m r)

note: the body of fmap doesn't mention ContT or runContT

instance Functor (via runContT @k @r @m) where
 fmap :: (a -> a') -> (((a -> m r) -> m r) -> ((a' -> m r) -> m r))
 fmap = flip (.) . flip (.)

instance Semigroup (via getAny) where
 (<>) :: Bool -> Bool -> Bool
 (<>) = (||)

instance Monoid (via getAny) where
 mempty :: Bool 
 mempty = False

@Icelandjack
Copy link
Copy Markdown
Contributor Author

(proxy means @(via Coercion @ty @viaTy) works and @(via id) is a no-op)

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented May 6, 2019

import Data.Semigroup renaming
 ( (+)    from (<>) @(via Sum)
 , (*)    from (<>) @(via Product)
 , (&&)   from (<>) @(via All)
 , (||)   from (<>) @(via Any)
 , (<|>)  from (<>) @(via Alt)
 , union  from (<>) @(Set _)
 , union' from (<>) @(Map _ _)
 , const  from (<>) @(via First)
 , tsnoc  from (<>) @(via Last)
 , liftS  from (<>) @(via Ap)
 )

import Data.Foldable renaming
 ( mapConcurrently_ from traverse_ @_ @(via Concurrently) )

import Data.Traversable renaming
 ( mapConcurrently from traverse @_ @(via Concurrently) )

import Control.Monad renaming
 ( filter                 from filterM       @(via Identity)
 , mapAndUnzip            from mapAndUnzipM  @(via Identity)
 , replicate              from replicateM    @(via Identity)
 , zipWith                from zipWithM      @(via Identity)
 , replicateConcurrently  from replicateM    @(via Concurrently)
 , replicateConcurrently_ from replicateM_   @(via Concurrently)
 )

@nomeata nomeata force-pushed the master branch 2 times, most recently from 6b33e58 to e7fdbc7 Compare June 11, 2019 12:04
@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Jul 1, 2019

  • Richard in ghc-devs thread discusses

    So you're viewing complete sets as a type-directed property, where we can take a type and look up what complete sets of patterns of that type might be.
    Then, when checking a pattern-match for completeness, we use the inferred type of the pattern, access its complete sets, and if these match up.

    I want to explore -XMatchingVia focusing on matching as a type-directed property; possibility: a type can be matched by any constructor of the (representationally) same type. How it relates to completeness and GADTs.

I'm not very excited about -XImportingVia but -XInstancesVia is handy.

@TheMatten
Copy link
Copy Markdown

May I ask, what's the current status of this proposal? Here and there, cases pop up in my code where ApplyingVia would make newtype transformations much more concise and readable.

@tysonzero
Copy link
Copy Markdown

Personally this feels like an abuse of typeclasses.

Typeclasses are specifically designed around coherence and having a canonical instance, whereas regular value-level Haskell is designed around composing and building pieces in incredibly flexible ways.

This seems like trying to force typeclasses to do things they are not designed for, and I don't find it very readable or beginner friendly.

Here is a blog post I wrote relating to this that I think is quite relevant to this proposal.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Can you turn your blog post into a proposal tysonzero

@tysonzero
Copy link
Copy Markdown

tysonzero commented Nov 17, 2019

@Icelandjack

To be honest part of my proposal is just to ask the community to not overuse typeclasses, and try and build types first, and then add typeclasses when canonical/globally-coherent values of these types are needed. For example I am sad that Monoid/Group/Ring are much more available as classes than as types.

With that said, actually doing the above is more awkward then it needs to be, so I can definitely put together a proposal to go after the current ergonomic issues:

E.g.

class FooBarBaz a where
    foo :: Foo a
    bar :: Bar a
    baz :: Baz a

Is ergonomically nicer but less useful than:

data FooBarBaz_ a = FooBarBaz
    { foo_ :: Foo a
    , bar_ :: Bar a
    , baz_ :: Baz a
    }

class FooBarBaz a where
    fooBarBaz :: FooBarBaz a

foo :: FooBarBaz a => Foo a
foo = foo_ fooBarBaz

bar :: FooBarBaz a => Bar a
bar = bar_ fooBarBaz

baz :: FooBarBaz a => Baz a
baz = baz_ fooBarBaz

I would like the former to enable me to do everything I can do with the latter, just like the underlying core, although of course without any of the _ namespacing hacks. I think RecordDotSyntax is somewhat of a prereq for getting particularly far with this.

I also have some more things to consider such as how to deal with superclasses, as they make it so the structure you can get out of an instance is larger than the structure you have to put in to make the instance in the first place, assuming the superclass instances have already been defined.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Apart from the community changing did you look at Coherent Explicit Dictionary Application for Haskell? You both propose a dictionary solution, I think it's the wrong way but it sounds like it solves what you want

@tysonzero
Copy link
Copy Markdown

tysonzero commented Nov 19, 2019

I haven't read through it particularly thoroughly but it doesn't seem overly relevant. I am not looking for coherence or global uniqueness, I want different behavior based on what parameter you pass in, which is pretty antithetical to the former (and to typeclasses in general).

I just don't see the appeal of using something that is essentially a much less flexible and powerful version of passing in a parameter. Particularly when the syntax and desugaring is quite beginner unfriendly and magical.

There are a variety of useful things that you can do with a parameter that will be quite problematic when trying to use ApplyingVia instead. Some examples off the top of my head:

-- parameterized structures
foo :: (Foldable f, Integral a) => f a -> a
foo = fold_ (additiveModulo 7)

You could technically do the above using type level naturals, but this will not generalize to more complex parameters.

-- deferring the choice of structure
foo :: (Foldable f, Integral a) => a -> f a -> a
foo n = fold_ (additiveModulo n)

You could do the above using a parameter at the type level, but then you are in for some serious pain if you want to get that value based on information present at runtime.

-- use two different structures in a single function
foo :: Num a => Monoid_ a -> Monoid_ a -> a
foo x y = fold_ x + fold_ y

Not sure how you would go about using ApplyingVia in the above, as there is only one a to play with.

-- Make a structure out of two structures
mkSemiring :: Monoid_ a -> Monoid_ a -> Semiring_ a
mkSemiring = ...

This seems like it would be pretty painful, as Sum and Product and so on operate on just one underlying type and its instances. You'd need a newtype that takes in two type variables for each of the monoid instances, plus probably a type variable for the underlying type that is actually stored, it must also then require that all 3 of these are coercible to each other.

-- First class usage of these structures
myIntMonoids :: [Monoid_ Int]
myIntMonoids = [additive, multiplicative]

This is just a small example, but the general idea is that these instances are very much second class citizens. You can only do the hard coded things with them that various extensions say you can do. They aren't real values.

I'm sure the above list is far from complete, value-level Haskell has a massive amount of power and flexibility.

I enjoy higher order functions and algebraic structures and am very happy that they are a big part of Haskell, and all of this feels like a step in the opposite direction. I want to be able to talk about things like monoids and rings and serializers and monads as first class values. I then also want to seamlessly transition from these first class objects to and from instances, when the convenience and power of global coherence and canonicity are beneficial to me. I don't want to have to only talk about one single blessed instance per type, and then try to carefully manage a variety of extensions to coerce them from one to another.

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Icelandjack commented Nov 19, 2019

I just don't see the appeal of using something that is essentially a much less flexible and powerful version of passing in a parameter.

You can't beat values in terms of ease of use, people like Gabriel have written about this. Why not use first-class values, I will try to answer these questions in a blog post but I think the choice is not obvious

@Icelandjack
Copy link
Copy Markdown
Contributor Author

Your examples are possible except the last one which doesn't have to do with the extension, Dependent Haskell will add a lot of power

@tysonzero
Copy link
Copy Markdown

tysonzero commented Nov 19, 2019

Your examples are possible except the last one which doesn't have to do with the extension,

Can you show me what they would look like?

Dependent Haskell will add a lot of power

I like dependent types, but this is absolutely not where I want to use them. \n -> fold_ (additiveModulo n) is simple stuff that shouldn't require dependent types (and on a related note should infer without a type annotation).

@LeanderK
Copy link
Copy Markdown

LeanderK commented Nov 19, 2019

Just to give another perspective. I am really excited for the proposal and never ever wanted more value-level haskell via dictionary passing like @tysonzero is proposing, I just feel like it doesn't address the problems i have. I see applying-via as a great tool when a single blessed instance is not possible and we have the situation that once we have chosen the correct instance, we want to use it coherently in the function we're calling. Passing dictionaries is more flexible, but I (almost?) never want the full flexibility. Once i've committed, i only want to use the single instance.

Working with type-classes makes writing abstract code so much more enjoyable. I can say: I have an abstract thing with property 1,2,3 and here's what I want to do with it. It's really a joy to write. But type-classes show their pain when the thing you've got have multiple, equally important instances. All those new types makes code really hard to read and understand. For me, applying via solves a real problem. It also might be that the other 100 times I call the function the default instance is the correct one and I don't have the problem of choosing. Which mean I don't want to refactor the function.

Since I am doing some computational math with Haskell, the perfect example is the ring with two possible monoid-instances: One for addition, one for multiplication. I explicitly don't want dictionary passing, any additional flexibility can actively harm me, I still want to rely on coherency when writing abstract code. But I have two equally important instances when the thing i have is a ring.

For me, a blessed canonical instances works 95% of the time, but it sometimes happens that I have to juggle a lot of newtypes. This proposal would really reduce the pain and i really hope it moves forward!

@tysonzero
Copy link
Copy Markdown

@LeanderK

I'm not proposing getting rid of typeclasses or typeclass-based functions. I'm just saying that when you want that flexibility you should just accept a parameter, and whenever you want coherence instead then use typeclasses.

ApplyingVia is a weird hybrid where you are using a bunch of code generation to get something that feels like an incoherent instance to get custom local behavior, but that technically doesn't directly involve incoherence due to all the implicit coerces.

@LeanderK
Copy link
Copy Markdown

LeanderK commented Nov 19, 2019

@tysonzero while i may be not as clear as i wanted to be, but i read your blogpost and your posts. I might have misunderstood something.

ApplyingVia is a weird hybrid where you are using a bunch of code generation to get something that feels like an incoherent instance to get custom local behavior, but that technically doesn't directly involve incoherence due to all the implicit coerces.

i was trying to argue that this is in fact sometimes very useful to the, because it complements the usually typeclass-mechanism (with the ring-example). It feels (to me) like your description has a negative connotation, but it's exactly what i argue is very useful to me and why. Sometimes, the type class solution just works perfectly for 95% of use-cases and then you have a thing (like the ring) without a canonical instance and now you're in pain because of all the newtypes. This would really help me in this situation, since I won't change it to a non-typeclass based method (because it works so well for the other 95% or passing dictionaries would be quite painful to write). This really happened to me and it looks like would also benefit other (theMatten mentioned he would like to have it).

@masaeedu
Copy link
Copy Markdown

masaeedu commented Mar 17, 2021

@Icelandjack

Can you turn your blog post into a proposal tysonzero

I can't speak for @tysonzero but perhaps #324 is something like what you're looking for?

For whatever little it's worth I am strongly in the camp of avoiding further incremental patching of the deriving mechanism. There's an incredible wealth of use cases for "deriving" instances that cannot be served by the baked in notion of representational type equality available in the compiler. Sometimes you want to map an instance along an isomorphism, sometimes merely along a homomorphism, and very often these morphisms are not statically known (or at least it is inconvenient to shunt enough of the logic into the type system to reify them statically).

IMO the best and most beautiful features of any system are the ones that emerge spontaneously from the interaction/composition of simpler features. I think that DerivingVia and this proposal are good workarounds given the state of affairs of the typeclass feature today. But if we need to keep adding DerivingThis, DerivingThat, and DerivingTheOther, and each such feature shunts a little more of the user's logic into the compiler, perhaps it is a good time to pause and reflect on whether a radically different approach is justified, at least before we proceed too far along this road.

@LeanderK
Copy link
Copy Markdown

LeanderK commented Oct 5, 2023

@Icelandjack I always come back thinking about this proposal. I personally really like it.

I wonder....can this be implemented as an GHC-plugin? I am not exactly sure as it's a bit over my head...but I that it's possible.

@ashokkimmel
Copy link
Copy Markdown

I made a plugin that did this:
coerceVia ::forall c a b. Coercible a b => a -> (c => b) -> b
coerceVia a _ = a

It would turn

sort @(Int `via` Down Int) [1,2,3]  

into

coerceVia  (sort @Down Int) (sort @Int) [1,2,3]

However if an instance doesn't already exist, like with Monoid Int

foldMap @_ @(Int `via` Sum Int) -> coerceVia (foldMap @_ @Int) (foldMap @_ @(Sum Int))

This doesn't work, as coerceVia doesn't successfully eliminate the Monoid Int constraint, leading to a No instance for Monoid Int error. I'm currently transforming the parsed result, so I don't know how to fix this error.

By the way, this compiles.

coerceVia @(Monoid Int) (foldMap @_ @Int) (foldMap @_ @(Sum Int))

Any ideas for where to go next?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.