Equivalent of Rust's From ↔ Into trait in Haskell?

Is there any support in standard Haskell (e.g. some typeclass) for generic conversion between types?

I really like what Rust’s standard library offers, esp. when implementation of From automagically implements Into for free. Dtto for TryFrom & TryInto.

I often run in situations, where parsed input has to be converted to my custom data types, do some computation on them, then output back as original plain text/json/whatever. I did always have to resort declaring my adhoc typeclass & instances again and again…

4 Likes

It’s not built-in but there’s:

Although it doesn’t do the automatic inversion and I don’t think many people use this for things like JSON conversions.

Edit: Oh, I was very confused as to how the Into could be automatically implemented if you have From, but I guess it just inverts the arguments. I was thinking it meant Rust would magically invert the function to get the reverse mapping. I guess the witch package solves this by just having the one class and not both.

5 Likes

I think this is an area where idiomatic Rust diverges from idiomatic Haskell.

I think the recommended way to do this in Haskell is to either have specific typeclasses for your use-case, eg, aeson’s ToJSON/FromJSON, or to just use plain functions rather than typeclasses, eg, parseFoo :: Text -> Maybe Foo.

I think in practice if you tried to recreate the Rust idiom in Haskell you would find that you get lots of unhelpful error messages because your types would become too general. There’s also probably other reasons why it hasn’t caught on as much in Haskell. Maybe different type-checking algorithms, laziness, and nicer closures also contribute for instance.

Though the extent of typeclass use tends to be a divisive topic in Haskell API design. I think there’s no clear consensus either way.

7 Likes

These sorts of typeclasses assert that there is at most one privileged way to convert from one type to another, which is true much less often than it first appears. My experience writing web APIs using aeson tells me that it’s an easy way to have your API formats depend on random instances declared on deep internal data types unless you’re really careful, and it makes it easy for innocent-looking changes to alter the actual API format. Under Haskell’s type system, a typeclass method with like from :: a -> b often leads to ambiguous type errors when you try to use it, because you can’t put a fundep on either end.

For simple conversions from one type to another, I’d probably declare an optic: usually an Iso, sometimes a Prism, or (rarely) a Lens. This gives you a single value which can be used to implement both directions of the conversion, as well as “temporarily look at your value in another way, perform an operation, then convert it back”.

10 Likes

Thanks for suggesting Witch! I’m the author.

The announcement post might provide some useful context:

Witch doesn’t aim to replace the more specific type classes from libraries like Aeson or Parsec. Instead it’s meant to provide an easy (and safe) way to convert between types like Int and Word16. And also between primitive types like Text and your application specific types like Name.

1 Like

It’s not just “hard” to do things correctly, the libraries are explicitly designed for people to do things wrong.

aeson not only expects its users to use Generic instances, it’s handrolling experience outright sucks. Even running a handrolled parser is a puzzle because the library does not bother exposing a Parser a -> LazyByteString -> Result a function.

aeson additionally expects every JSON to be 1:1 convertible to its Haskell counterpart (Value). This makes streaming out of the box impossible in either direction and provides no value whatsoever to the user, who could just use the original LazyByteString.

servant doubles down on the “one true instance” idea by assigning a content type to a typeclass. As a result any production API that uses servant is guaranteed to use aeson's Generic instances to parse and compose one-direction endpoint types, making minor adjustments impossible without manually rewriting the instances.

aeson criticisms apply to virtually every single codec package in the ecosystem, the servant one is the general line of thinking when designing Haskell applications. The fascinating thing is that all of the bad choices here are at library level, the language provides all the basic tools to do things right.

4 Likes

I agree with your high-level point but disagree on some of the details. I think codec typeclasses became popular because they are really convenient and almost right until you hit the situations where they’re really, really wrong. Things like instance FromJSON a => FromJSON [a] seem really nice and do what you want, until you hit the problems I outlined earlier.

Servant gets a pass from me because it brings so much other safety and you need to associate a codec with a data type. It also uses separate tags for different Content-Type values: the JSON type is what makes it demand ToJSON/FromJSON instances at the use site.

You can sometimes get the benefit of type-directed codec selection without forcing yourself into a single way of serialising/deserialising data types: waargonaut's JsonEncode class takes an additional type parameter to namespace different codecs. It’s clear that this feature is to let it integrate with type-driven libraries like servant and not to be the default way of doing things.

This is a slight exaggeration because Parser is its own weird result-ish type:

A JSON parser. N.B. This might not fit your usual understanding of “parser”. Instead you might like to think of Parser as a “parse result”, i.e. a parser to which the input has already been applied.

Then you can decode your LazyByteString into a Value and feed your hand-rolled parser to Data.Aeson.Types.parseEither :: (a -> Parser b) -> a -> Either String b. But it’s definitely not encouraged. That you can’t stream your parsing is a different problem and exists whether or not you use codec typeclasses.

5 Likes

The alternative is well-documented conversions similar to how Data.ByteString.Builder does it and the only downside it comes with is it’s slightly more verbose (which is inevitable because it conveys extra meaning).

My contention is that an endpoint should merely be a declaration that says “it’s JSON, decoded using this codec”. Mixing in typeclasses means the codec is most probably in a module half across the codebase, same with every other codec this codec depends on.

This is the outcome of trying to shove everything into the type-level: you either have to define a new wrapper type for every single endpoint (and do the busy work of filling it in) or you get bad design.

I do not see that a good excuse and I do not think there’s a good reason for it to be structured that way. I spent a couple of months writing my own JSON parser just to prove that to myself.

I strongly believe any generalizations should be separated from the barebones codec libraries because they are inevitably less expressive than handrolling. Having JsonEncode tag a seems silly when it should be two separate libraries: one defining a generic Encode tag a, one directly depending on waargonaut and supplying a Waarg.JSON tag.

2 Likes

Thanks for mentioning that! I contributed it to the documentation after becoming baffled about what Parser actually was.

I find the aeson API almost entirely too baffling to use in practice, but I have recently discovered there seems to be a simple way to use it to hand-roll parsers, specifically

  • Start with a Value
  • To parse the Value use parseJSON to turn it into an Object, Array, or primitive Haskell type
  • To parse an Object either
    • use .: to access its fields (if it corresponds to a Haskell record) – because .: is return-type polymorphic it automatically parses to the type expected by the next stage of your parser
    • use for to parse its values (if it corresponds to a Haskell map)
  • To parse an Array use for to parse its elements

I haven’t done much with this, but it seems to work well in simple cases. What do you think?

This function is yet another one from the aeson API that falls into the “baffling” category. Why is it not just Parser a -> Either String b?

Have you looked at autodocodec? As I understand it, it is designed exactly to solve this problem: it supports only hand-rolling and is supposed to make that use case easy. (I haven’t used it myself.)

I imagine it was because Parser is “a funny sort of ‘parse result’”, so you give it a function which parses whatever string type came in and looks a bit more normal. Except by parametricity it’s equivalent to Parser b -> Either String b, isn’t it?


There’s probably an interesting second thread here, about how to structure codec libraries, splitting the design along two axes:

  1. Typeclass-driven vs. “pass your codec values around manually”. aeson, cereal, and autodocodec are examples of the former; hasql, opaleye and waargonaut are examples of the latter.
  2. How codecs are combined: are you expected to rely on instance resolution to derive parsers for complex structures (e.g., instance FooCodec a => FooCodec [a]) or are you expected to use combinators to put parsers together (e.g., list :: Codec a -> Codec [a])? Typeclass-diven libraries tend to use the former and “pass-the-codec” libraries must use the latter (plus support from standard abstractions: encoders tend to be combined with a mix of Semigroup+Contravariant or Contravariant+Divisible+Decidable; decoders using Applicative and sometimes Monad; sometimes product-profunctors comes into play as you know).
    • API weirdness often creeps in when you have to us combinators to wire up your typeclasses, like when you try to use aeson but you don’t want every object to be its own record type.
1 Like

Yes

Yeah, it’s a really important discussion, A lot of the typeclass-driven libraries are really hard to use because it’s difficult to find the underlying value-level implementations. Sometimes they don’t even exist. Below is a case in point. postgresql-simple has

instance (FromField a, Typeable a) => FromField (PGRange a)

which is morally a function

 fromFieldRange :: Typeable a => FieldParser a -> FieldParser (PGRange a)

but previously the API didn’t provide any way to access that function, so I had to submit a PR to expose it.

On the other hand, using value-level libraries can require writing repetitive code, as a dose of type classes can reduce the burden.

I just use flip (withObject "blah") and similar for nesting, custom functions where it makes sense and regular primitives for everything else. It merely sucks, it’s not impossible to use.

The only possible answer is backwards compatibility, all of those functions combine the two arguments immediately.

No, but I’m also not a fan of higher-level codec libraries in general, they disregard the “parse, don’t validate” principles. Even the simplest bidirectional encoding, as convenient as it might seem, imposes very heavy constraints, either making backwards compatibility impossible or highly tedious.

I’m on team boilerplate for everything codec-related.

1 Like

Yeah, that’s what I used to do, but I’m much happier with the lightweight process I described above.

You might be interested in [my shameless plug of] json-spec, which allows you to specify the shape of the JSON at the type level, whereupon it will do most of the tedious work for you (Not all of the work of course. Similar to how a servant api allows servant-server to do most of the tedious HTTP-related work for you, but you still have to write the essence of your server). I guess technically this means “making minor adjustments” still means “manually rewriting the instance”, but:

  • It’s a lot easier (to me, at least) than writing instances using hand-crafted aeson codecs,
  • You have a type level representation of the JSON shape[1], which you can use for all the purposes for which you might have been using Generics, such as automatic doc and code generation (e.g. json-spec-openapi, and json-spec-elm-servant)
  • You can choose your own, I guess, “composition strategy” (for lack of a better term) to suite your needs. What I mean by this is with Generics you are forced into pretty much mirroring Haskell types, and nested structure in your JSON data means necessarily nested Haskell types. With json-spec you can choose to mirror haskell types if you like, in which case changes to types deeply nested within your api types will cause api changes, like Generics. But you can also choose to to specify the entire spec directly on the top-level api type, ensuring that you can’t change the encoding of the nested types without a type error.

The biggest problem for me is that as soon as I switch away from Generics into hand-rolled aeson instances, I lose all of the other tooling that comes along with Generics. Allowing myself to have fine-grained control over the json encoding while still being able to e.g. generate documentation is the reason I wrote json-spec. Also, contributions are welcome! I feel like the json-spec family of packages still has some maturing to do.

[1] I don’t want to say “schema” because json-spec does not precisely translate to the technology named “Json Schema”.

The “everything on the type level” bit is a weird one, you’re pretty much just evaluating a whole separate datatype generated from a template and then restructuring that into whatever type you need. Not that far from Generics from my point of view.

There definitely exists potential for a sweet spot if someone figures out how to make that conversion zero-cost, perhaps by moving the template to the data level and using codecs directly instead of hardcoding types. I however feel like that would be a lot of work and the side cases won’t generalize.

Tests, for all the contempt they get, have one property no type-level solution would ever provide: they’re independent from code and thus backwards compatible. I don’t know if there even is a point in trying to invent a do-it-all abstraction when all the boilerplate could be delivered in a mere fraction of that time.

Well, I guess the mechanism might be similar to Generics if you squint, but I’m not sure if you’re also saying the user experience is similar. Because I really don’t think the user experience is similar at all. For instance, while you can specify a Generic instance for a type by hand, even going so far as to include representations of nested types, it probably isn’t wise or encouraged.

The intent of the generics mechanism is that your Generic instances will be auto-derived, producing a “type level DSL” (Rep a) that more or less exactly represents the structure of your Haskell type, and moreover that Rep a is bidirectional. Whereas the intent of json-spec is the exact opposite. You are encouraged to manually write two JSON-focused "type level DSL"s that describe the encoding, and, separately, the decoding of the JSON you want irrespective of the structure of your Haskell type.

1 Like

I tried to provide both in toml-reader, but the primary interface is the codec; the typeclass just blesses a codec as the default.

After some time, I don’t think I like the way I implemented it. The API is just awkward to use, especially around combinators. One of these days, I might revisit/revamp the API