question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add Invariant Abstraction

See original GitHub issue

Description

Add support for an abstraction that combines a Functor and a Contravariant in a way similar to a Profunctor, but where the output parameter is constrained to the input parameter.

From Haskell:

class Invariant f where
  invmap :: (a -> b) -> (b -> a) -> f a -> f b

This is a useful abstraction when dealing with bidirectional programming, for example:

type Decoder<'a> = (string -> Result<'a, string>)
type Encoder<'a> = ('a -> string)
type Codec<'a> =
  { decoder : Decoder<'a>
    encoder : Encoder<'a> }

type Decoder<'a> with
    static member Map(d : Decoder<'a>, f : 'a -> 'b) : Decoder<'b> = map f << d

type Encoder<'a> with
    static member Contramap(e : Encoder<'a>, f : 'b -> 'a) : Decoder<'b> = f >> e

type Codec<'a> with
    static member Invmap({ decoder = d; encoder = e } : Codec<'a>, f : 'a -> 'b, g : 'b -> 'a) : Codec<'b> =
        { decoder = map f d
          encoder = contramap g e }

A similar related abstraction is an Invariant2, a sort of Bifunctor analog of Invariant

class Invariant2 f where
  invmap2 :: (a -> c) -> (c -> a) -> (b -> d) -> (d -> b) -> f a b -> f c d

Which has a similar application, except that it allows the definition of types like Codec<'a, 'b>, where the encoded type is parametric in addition to the decoded type.

If you like the suggested additions, I would be happy to submit a PR with these abstractions added.

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:18 (14 by maintainers)

github_iconTop GitHub Comments

3reactions
jhbertracommented, Apr 10, 2018

I know what you mean when you say it’s hard to immediately see the real-world value of these abstractions. I find this is often the case with functional abstractions, until you actually encounter a situation where they’re useful.

I have encountered a situation recently in one of my personal projects Data Blocks where this abstraction would be useful.

It is a more specific example of the sample code I gave above:

type JsonDecoder<'a> = JsonDecoder of (Json-> Result<'a, string>)
type JsonEncoder<'a> = JsonEncoder of ('a -> Json)
type JsonCodec<'a> =
  { decoder : JsonDecoder<'a>
    encoder : JsonEncoder<'a> }

The basis of the API is that for every operation you define for the JsonDecoder, there should be a corresponding operation defined for the JsonEncoder. e.g.:

let stringDecoder = JsonDecoder (function JsString s -> Ok s | _ -> Error "Expected a string value")
let stringEncoder = JsonEncoder JsString

let boolDecoder = JsonDecoder (function JsBool b -> Ok b | _ -> Error "Expected a boolean value")
let boolEncoder = JsonEncoder JsBool

let mapDecoder f (JsonDecoder d) = JsonDecoder (f << d)
let contramapDecoder f (JsonEncoder e) = JsonEncoder (f >> e)

given that the two sets of APIs are opposites, this allows a single codec API to be written that combines these:

let string = { decoder = stringDecoder; encoder = stringEncoder }
let bool = { decoder = boolDecoder; encoder = boolEncoder }
let invmap f g codec = 
  { decoder = mapDecoder f codec.decoder
    encoder = contramapEncoder g codec.encoder }

invmap is therefore required by the API, because it’s the only way to combine a functor and a contravariant functor into a single unit that still allows mapping over the type parameter.

As a result of this, you only need to specify your JSON schema one time, when creating the codec:

// assume: val contact : Name -> string -> string option -> Contact

// Instead of
let contactDecoder : JsonDecoder<Contact> =
    contact
    <!> requiredDecoder "name" (stringDecoder |> mapDecoder Name)
    <*> requiredDecoder "email" stringDecoder
    <*> optionalDecoder "phone" stringDecoder

let contactEncoder : JsonEncoder<Contact> =
    object
    |> propertyEncoder "name" (fun c -> c.name) (stringEncoder |> contramapEncoder (fun Name n -> n))
    |> propertyEncoder "email" (fun c -> c.email) stringEncoder
    |> propertyEncoder "phone" (fun c -> c.phone) (nullableEncoder stringEncoder)

let contactDecoded = runDecoderString contactDecoder "''{"name":"John Doe", "email":"john@doe.com"}"''
let json = runEncoderString contactEncoder contactDecoded


// We can do:
let contactCodec : JsonCodec<Contact> =
    contact
    |> ``{``
    |> required "name" (fun x -> x.name) (string |> invmap Name (fun Name n -> n))
    |> required "email" (fun x -> x.name) string
    |> optional "phone" (fun x -> x.name) string
    |> ``}``

let contactDecoded = decodeString contactCodec "''{"name":"John Doe", "email":"john@doe.com"}"''
let json = encodeString contactCodec contactDecoded
1reaction
jhbertracommented, Apr 10, 2018

| I really like the example you wrote, I wish we had more sample code and docs like that.

Could always look into adding some! I’ve started using this library in a number of projects, I’d be happy to contribute code examples. I agree, examples are by far the most effective way to communicated the purpose / application of an abstraction. I think I’ve even got a few potential applications of Arrow

Also had a look at your project, looks interesting, are you porting an existing Haskell project?

This is not a port of a Haskell project, in fact it’s quite heavily inspired by Elm’s Json.Decode and elm-decode-pipeline. The idea behind the DataBlocks project is to provide a similar interface, but with bidirectional capabilities (can encode or decode from the same spec).

In addition, the intent was not for this to be restricted to JSON. I’m working on genericizing it so that the building block is an Epimorphism that bidirectionally converts from any type to any other type (where the decode direction may fail). This means it can be combined like this:

val jsonCodec : DataBlock<Json, 'a> // can decode and encode JSON data
val jsonSerializer : DataBlock<string, Json> // can parse and write raw JSON

// it's an instance of Category
let jsonStringCodec : DataBlock<string, 'a> = jsonCodec <<< jsonSerializer

val xmlCodec : DataBlock<Xml, 'a> // can decode and encode XML data

// Since it's invariant in both type arguments, it is also a bidirectional category
let jsonXmlConverter : DataBlock<Json, Xml> = jsonCodec ``some additional compose operator`` xmlCodec

// Should be able to define an async version as well
val sqlCodec : AsyncDataBlock<SqlQuery, 'a>
let persistJson : AsyncDataBlock<Json, SqlQuery> = jsonCodec ``some operator`` sqlCodec 

Btw this abstraction smells a bit to some optic stuff, like ISOs, isn’t it?

It’s related! An Invariant is a functor over which an isomorphism can be mapped.

We might need to discuss how the (implicit) typeclass hierarchy will be updated with this abstraction, specially to define the defaults.

I suspect Haskell and PureScript docs would be a good place to start for this.

Finally I wanted to point you to another project: https://github.com/mausch/Fleece which is heavily inspired in Aeson and we’re trying to revamp. It seems to be related to the code you posted, I would like to hear your thoughts about it.

There is another Aeson-based F# project called Chiron which seems to be quite similar in terms of API and implementation.

I do really like the fact that the typing is implicit! That makes the code a lot more terse. I’ll have to look over it in more detail 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

Reading 13: Abstraction Functions & Rep Invariants
An invariant is a property of a program that is always true, for every possible runtime state of the program. Immutability is one...
Read more >
CSE 331 / Writing Abstraction Functions and Rep Invariants
Abstraction functions and representation invariants are internal documentation of a class's implementation details. A client should not need any of this ...
Read more >
Abstraction Functions and Representation Invariants
In the case of an ADT, the rep invariant is a module invariant. Module invariants are useful for understanding how the code works,...
Read more >
Rep Invariants and Abstraction Functions
Representation Invariant: A condition that must be true over all valid concrete representations of a class. The representation invariant also defines the domain ......
Read more >
Representation Invariants and Abstraction Functions
The abstraction function maps valid concrete data representation to the abstract value it represents. • I.e., domain is all reps that satisfy the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found