Generic examples - Haskell

Welcome to the Functional Programming Zulip Chat Archive. You can join the chat here.

Vladimir Ciobanu

What are some good/motivating/unexpected Generic examples?

TheMatten

Generic church encodings :big_smile:

TheMatten

That's unexpected one :slight_smile: - I guess generic Show, Read or ToJSON instances could be considered motivating - they allow you to get rid of lot of boilerplate by treating datatypes uniformly

Vladimir Ciobanu

I mean, Show and Read would be interesting if GHC didn't already automatically derive them. I am probably asking for too much without even explaining the context: trying to do a short presentation for the junior devs at the office on Generics, so I wish I could find some good motivating example to use.

The current contender is Eq since it's easy to explain and implement, but then it's hard to motivate why since GHC already derives it. ToJSON might be a good example; I wonder how easy it is to implement.

Vladimir Ciobanu

I think most of them have already used Generics in the sense that they derived it and reaped the benefits of derive anyclass (...), but I don't think any of them wrote their own typeclass / instances using Generic so I thought an example might help.

TheMatten

I did simplified AsJSON impl for similar purposes (it just didn't end up being used :slight_smile:)
I think that one is a good candidate, because it feels more real-world-like, works with both directions (to and from generic rep) and encoding of sums is a little bit more creative than just spitting out value corresponding to some constructor, while still simple enough

TheMatten

(AsJSON as in custom class for going to and from Aeson.Value)

Vladimir Ciobanu

That sounds interesting, I'll have a stab at it and see how it ends up. Thanks!

Asad Saeeduddin

Reimplementing the derivations for Eq, Show, Functor etc. using generics would make for a pretty good blog post, I still have no idea how to do most of those

Vladimir Ciobanu

Eq is pretty trivial, but Functor might be rough. That's a good idea though!

Lysxia

GHC's stock deriving is not extensible though, as opposed to generics. You can take a generic implementation, which is just a function, and manually replace some of the cases, or even do "surgery" on your data types before deriving.

Lysxia

Monoid is another common example, that is not handled by stock deriving.

gilmi

I'd really love to see an implementation of uniplate using Generic but I actually don't know if that's even possible

Sandy Maguire

what do you use uniplate for?

Sandy Maguire

but also there are no instances for this thing, so i don't know if it's what you're talking about

gilmi

Compilers. Things like transforming every element of a certain type or getting all of the elements of a certain type

gilmi

I use the Data instances since it's the easiest

gilmi

But also not so fast

Sandy Maguire

yeah you could definitely generate these generically

gilmi

Are you familiar with resources that might help me understand how to implement this? I've read the chapter in your book and am currently slowing going through https://www.stackbuilders.com/tutorials/haskell/generics/

gilmi

But this is really all I found on the topic

gilmi

Specifically I'm unsure how to recurse through types and apply a function to an element of a specific type

Sandy Maguire

which specific type? something concrete? or like a recursive bit

gilmi

Like have exactly the api and behavior of transform or transformBi

gilmi

If I pass (Int -> Int) to transformBi it will find all Ints in the type and transform them

gilmi

Or through mutual recursion and stuff

Sandy Maguire

you'd do something like this:

class GTransform a struct where
  gtransform :: (a -> a) -> struct x -> struct x

then do structural recursion over struct, and give instances for instance {-# OVERLAPPING #-} a (K1 a) that performs the function and instance {-# OVERLAPPED #-} a (K1 b) that doesn't

gilmi

Ok I think I understand thank you!

Sandy Maguire

and then

transform :: (Generic struct, GTransform a (Rep struct)) => (a -> a) -> struct -> struct
transform f = to . gtransform f . from
Sandy Maguire

(or you could use generics to derive the uniplate class directly)

gilmi

Yeah that's also a good option

gilmi

Thanks for the help!

gilmi

Is overlapped a thing? I get unrecognized pragma warning

TheMatten

@gilmi Just overlapping by itself should work

gilmi

Ok I got transform to work and that's pretty awesome thanks for the help!

Sandy Maguire

liberal usage of INLINABLE will make that puppy hella fast too

gilmi

So this is what I got and I see very significant improvement over uniplate Data in my tiny benchmark
https://gist.github.com/soupi/c9048ca0d53c8a3c67f7f9a217748003

implementation of transform using generics. GitHub Gist: instantly share code, notes, and snippets.
gilmi

Does that make sense? Also, how can I get away from defining the Expr Int instances manually, if I don't do that I get a no Generic instance for Int

TheMatten

@gilmi You only want overlappingon GTransform a (K1 _1 a) case

Sandy Maguire

yeah, seeing a perf improvement makes sense

Sandy Maguire

if you look at the core representation i bet the generic transform is implemented as a case on the data structure that replaces it with the new value

Sandy Maguire

versus uniplate which i would be surprised if it gets optimized away

Sandy Maguire

but yeah, you only want this case to be oerlappying: instance {-# OVERLAPPING #-} Transform a a => GTransform a (K1 _1 a) where

gilmi

Ok cool, is that so I can't implement other instances?

Sandy Maguire

well you want the a a case to overlap the a b case

gilmi

Any ideas regarding the Int instances?

Sandy Maguire

i dont think you want those int instances

Sandy Maguire

transform should be just a function, not a method

Sandy Maguire

ah yeah, getting this thing recursive might be tricky

Sandy Maguire

why do you bind a in the class of Transform?

gilmi

Can it typecheck without it when I want to apply the function?

gilmi

Oh, I don't actually need that info there right?

Sandy Maguire

what semantics do you want here

Sandy Maguire

if you are targeting a structure that is recursive in itself do you want to hit the leaves or not

gilmi

I want to hit all occurrences of the type

gilmi

Leaves and also with mutual recursion with other types

Sandy Maguire

so in that case i think you want transform to be a method

Sandy Maguire

but with a universal a

Sandy Maguire

and then you give an overlappable Generic instance for it

Sandy Maguire

and you give specific Int and other primitive instances

Sandy Maguire

each of which ignores the function

gilmi

But then for example transform (+1) [1,2,3] == [1,2,3] because you ignore the function right?

Sandy Maguire

in the a (K1 a) instance you recurse, and then apply the function you have

Sandy Maguire

so the recursion fails, and then you apply the function you hvae :)

gilmi

Oh! I understand.

gilmi

Thanks for all the help, I really appreciate it.

gilmi

I'm not exactly sure how to fix the issue here but I'm getting 'two instances involving out of scope types' error
https://gist.github.com/soupi/c9048ca0d53c8a3c67f7f9a217748003
What am I doing wrong?

implementation of transform using generics. GitHub Gist: instantly share code, notes, and snippets.
gilmi

Also had a problem trying to define the overlapping generic instance for Transform

Lysxia

I think the uniplate business this is also doable by reusing generic-lens: https://github.com/kcsongor/generic-lens/issues/103#issuecomment-570422982

Hi! First I'd like to thank you for this super cool package! I was wondering why doesn't types recurse deeper when the Traversal focus has the same type as the "big" outer type. E...
gilmi

I opened that issue, I remember having some trouble with generic-lens that I couldn't figure out how to implement transform and universe using it. I was under the impression that the identity traversal for the same type case was a deal breaker but maybe i was mistaken

Lysxia

I see. You're right something probably needs to be changed; my point was more that the necessary mechanisms are already there, so it should be possible to avoid reinventing them.

Vladimir Ciobanu

Is there a simpler solution to defining generic aeson instances for types with the same shape but different field names? Like my json is { "id": 1, ... } but my type has a nodeId :: Int, ... field. I wish I could wrap the Int in some newtype and derive via (JsonFieldName "id") or something. I guess I could also do this with generic-data-surgery by removing and re-adding the field with the different name...?

Vladimir Ciobanu

I should probably have started a new thread.

Lysxia

There's something to rename fields in the options you pass to genericParseJSON/genericToJSON

Vladimir Ciobanu

Guess I could use that to write JsonFieldName myself.

Vladimir Ciobanu

... but then that function is just some String -> String and I don't get any compile-time errors. :(

Would it be possible to do something like...

data T = T
    { tOne :: String
    , tTwo :: String
    , three :: String
    }
    deriving (Generic)
    deriving ToJSON via (RenameFields '[ '( "tOne", "one" ), '( "tTwo", "two" ) ] T)
Vladimir Ciobanu

... which would result in an instance as if the type had the fields one, two, and three?

TheMatten

@Vladimir Ciobanu You could possibly demote that list to value level and do Data.List.lookup, sticking with current name if replacement is not found

Vladimir Ciobanu

But I want a compile-time error if tOne is not a field for T.

Vladimir Ciobanu

Maybe even if one is already a field, or if I do '[ '("tOne", "one"), '("tTwo", "one") ] as well.

TheMatten

Oh, actually - pass that list of replacements around in class that generates instance and give it some KnownSymbol (FindReplacement sym replacements) constraint, where FindReplacement is type family

TheMatten

And you want to collect all field names maybe upfront - then put family that checks those lists as constraint on top

Lysxia

@Vladimir Ciobanu I don't think such a thing exists in a library yet, but you can do it.

gilmi

I did manage to get a generic version of transform to work, thanks everyone for the help!
While it is faster than uniplate Data, I have a feeling it can be even faster.
@Sandy Maguire or anyone else, if you could take a look at my approach and tell me if I did anything stupid I'd greatly appreciate it. The implementation is at the bottom:

https://gist.github.com/soupi/53351593af29acf58064aac6d6609e94

And benchmark results:

https://gilmi.me/static/misc/generic-plate-results.html

Uniplate.Data vs hand written Lens.Plated vs generic-lens - Plate.hs
Sandy Maguire

nice! it would be interesting to turn on -ddump-simpl -dsuppress-all and look at why generics is faster

gilmi

I need to learn how to read this but it looks like this:
https://gist.github.com/soupi/3b84c2cb3bf81f9f629e3b563d7c9bd2

Plate dump. GitHub Gist: instantly share code, notes, and snippets.
Sandy Maguire

try again without the bench stuff

Sandy Maguire

oh, maybe there isn't any? what hs file does this correspond to

gilmi

I've removed those and uniplate as well. Only leaving one call to transform and the Expr because I didn't know if it might be relevant or not

gilmi

So basically there's the definition of Expr, the function opts from the benchmark and the function check that runs one transform on a simple expression, and all of the transform code

gilmi

I can upload that file if you like

gilmi

https://gist.github.com/soupi/ee85d03775c21ad2f9270062deeb1b80

Plate.hs src. GitHub Gist: instantly share code, notes, and snippets.
Sandy Maguire

get rid of the showing stuff also please

Sandy Maguire

no deriving, no showing

Sandy Maguire

just main = pure (transform opts) (Lit 1)

gilmi

Should I get rid of the exprssion entirely or not?

Sandy Maguire

i'd move the transform stuff into a separate module

Sandy Maguire

define expr with only generic

Sandy Maguire

and then main = pure (transform opts) (Lit 1)

Sandy Maguire

right now that core contains code for all of the stuff you're deriving, plus for the generic transformations, plus for how to do the error messaging, etc

Sandy Maguire

obfuscating the bit we want to look at, and i cbf to find it in that sprall

gilmi

Yeah I understand I'll upload the new files in a couple of minutes

gilmi

https://gist.github.com/soupi/3b84c2cb3bf81f9f629e3b563d7c9bd2

Plate dump. GitHub Gist: instantly share code, notes, and snippets.
gilmi

Updated the file, below there's Compiling Main

gilmi

Also I had to derive Generic

gilmi

Also this doesn't look totally alien to me

Sandy Maguire

:) looks like it was completely optimized away

gilmi

So basically this is the best we can get in your opinion?

Sandy Maguire

hahah well it is doing zero work

Sandy Maguire

so... hard to do better than that

gilmi

:) Maybe I'll try a more complex expression?

Sandy Maguire

i suspect it will also optimize away

gilmi

Wait did it do this at compile time??

Sandy Maguire

i suspect any constant expr you put there will optimize away

Sandy Maguire

try making one with a NOINLINE on it

gilmi

I added a read <$> getLine

gilmi

Updated the files

Sandy Maguire

actually try it without read

Sandy Maguire

just:

foo :: Expr
foo = Neg $ Neg $ Lit 5
{-# NOINLINE foo #-}

main = do
  pure $ transform opts foo
Sandy Maguire

noinline should prevent the const expr optimization

gilmi

Ohh I understand

gilmi

Well I can kinda read this but I'm not sure if this is the desired result

Sandy Maguire

hmm the generics aren't optiizing away

Sandy Maguire

oh that's probably to be expected

Sandy Maguire

actually i'm not sure whats's going on here

Sandy Maguire

it's been a while since i've looked at core

gilmi

Can ghc look at a L1 (R1 ..) and go 'oh! That's actually Neg'?

gilmi

I'll start reading the 'Optimizing Generics Is Easy!' paper, it looks relevant here.

gilmi

With -O2 instead of -O it looks different

Sandy Maguire

do all of the L1s disappear?

gilmi

I think they did

gilmi

I updated the gist

gilmi

I did try running the benchmark again with O2 and didn't see improvements though

Sandy Maguire

it's getting fully optimized again

gilmi

If I add getLine it's not removing the L1s anymore