Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Natural Transformation #2

Open
dotnetCarpenter opened this issue Jul 25, 2021 · 6 comments
Open

Natural Transformation #2

dotnetCarpenter opened this issue Jul 25, 2021 · 6 comments

Comments

@dotnetCarpenter
Copy link

dotnetCarpenter commented Jul 25, 2021

In the Fluture section, it could be useful to mention Natural Transformation as described in https://github.com/MostlyAdequate/mostly-adequate-guide/blob/master/ch11.md.

I find the following natural transformation of an Either to Future very useful:

// eitherToFuture :: Either a b -> Future a b
const eitherToFuture = either => Future ((reject, resolve) => {
  S.bimap (reject) (resolve) (either)

  return () => { reject () }
})

One use-case could be parsing JSON:

const log = msg => x => (console.log (msg, x), x)

const logError = msg => x => (console.error (msg, x), x)

const parseJson = S.encase (JSON.parse)

// futureParseJson :: Error a, Object b => String -> Future a b
const futureParseJson = S.pipe ([
	parseJson,
	log ('after parseJson'), // <- debugging
	eitherToFuture,
])

const getJson = fork
	(logError ('Rejection:'))
	(log ('Resolved:'))


// main

const validJsonString = '[{"foo":"bar"},{"foo":null}]'
const invalidJsonString = '[{"foo":"bar"}{"foo":null}]'

getJson (futureParseJson (validJsonString))
  // -> after parseJson Right ([{"foo": "bar"}, {"foo": null}])
  //    Resolved: [ { foo: 'bar' }, { foo: null } ]

getJson (futureParseJson (invalidJsonString))
  // -> after parseJson Left (new SyntaxError ("Unexpected token { in JSON at position 14"))
  //    Rejection: SyntaxError: Unexpected token { in JSON at position 14
  //      at parse (<anonymous>)

What do you think?

@dotnetCarpenter
Copy link
Author

dotnetCarpenter commented Jul 25, 2021

Actually, getJson can be composed with futureParseJson, so we get rid of the duplicated calls in the main section:

const log = msg => x => (console.log (msg, x), x)

const logError = msg => x => (console.error (msg, x), x)

const parseJson = S.encase (JSON.parse)

// futureParseJson :: Error a, Object b => String -> Future a b
const futureParseJson = S.pipe ([
  parseJson,
  log ('after parseJson'), // <- debugging
  eitherToFuture,
])

const getJson = S.pipe ([
  futureParseJson,
  fork
    (logError ('Rejection:'))
    (log ('Resolved:')),
])


// main

const validJsonString = '[{"foo":"bar"},{"foo":null}]'
const invalidJsonString = '[{"foo":"bar"}{"foo":null}]'

getJson (validJsonString)
  // -> after parseJson Right ([{"foo": "bar"}, {"foo": null}])
  //    Resolved: [ { foo: 'bar' }, { foo: null } ]

getJson (invalidJsonString)
  // -> after parseJson Left (new SyntaxError ("Unexpected token { in JSON at position 14"))
  //    Rejection: SyntaxError: Unexpected token { in JSON at position 14
  //      at parse (<anonymous>)

@dotnetCarpenter
Copy link
Author

dotnetCarpenter commented Jul 25, 2021

Perhaps unsubscribe should be mention as well but now it gets complicated...

// parseJson :: Error a, Object b => String -> Future a b
const parseJson = S.compose (eitherToFuture) (S.encase (JSON.parse))
// getJson :: String -> Function
const getJson = S.pipe ([
  parseJson,
  fork
    (logError ('Rejection:'))
    (log ('Resolved:')),
])

// main

const validJsonString = '[{"foo":"bar"},{"foo":null}]'
const invalidJsonString = '[{"foo":"bar"}{"foo":null}]'

getJson (validJsonString) // <- unsubscribe
  //    Resolved: [ { foo: 'bar' }, { foo: null } ]

getJson (invalidJsonString) // <- unsubscribe
  //    Rejection: SyntaxError: Unexpected token { in JSON at position 14
  //      at parse (<anonymous>)

On second thought, I think unsubscribe should have its own section...

@dotnetCarpenter
Copy link
Author

@Avaq just pointed out that eitherToFuture can be defined as:

const eitherToFuture = S.either (Future.reject) (Future.resolve)

From the Gitter chat:

Aldwin @Avaq:matrix.org [m] 16:36
@dotnetCarpenter: Side-note: You can define eitherToFuture like S.either (Future.reject) (Future.resolve).

@jceb
Copy link
Member

jceb commented Aug 24, 2021

@dotnetCarpenter thanks a lot. I'll work through your comments. Feel free to prepare a PR.

@dotnetCarpenter
Copy link
Author

dotnetCarpenter commented Aug 24, 2021

Thank for the attention @jceb. I still feel that I haven't articulated a nice overview of natural transformation with Fluture yet. Probably we need to brainstorm this together before it will get any good.

Having fork inside a function composition is probably an anti-pattern. Also a section on natural transformation should have more examples.

I'm not sure if swap is considered a transformation, since it does not change data type.

swap :: Pair a b -> Pair b a

and

//    swap :: Either a b -> Either b a
const swap = S.either (S.Right) (S.Left);

The latter can be used instead of S.ifElse to continue to try functions until one function returns a Right.

The example below was provided by @davidchambers.

parseImageType :: ImageType -> Buffer -> Either Buffer Image
//    parseGif :: Buffer -> Either Buffer Image
const parseGif = parseImageType ('gif');

//    parsePng :: Buffer -> Either Buffer Image
const parsePng = parseImageType ('png');

//    parseJpg :: Buffer-> Either Buffer Image
const parseJpg = parseImageType ('jpeg');

//    parseImage :: Buffer-> Either Buffer Image
const parseImage = S.pipe ([
  S.Right,                               // <- Right Buffer
  S.chain (S.compose (swap) (parseGif)), // <- Right Buffer | Left Image
  S.chain (S.compose (swap) (parsePng)), // <- Right Buffer | Left Image
  S.chain (S.compose (swap) (parseJpg)), // <- Right Buffer | Left Image
  swap,                                  // <- Left Buffer | Right Image
]);

Note that Sanctuary does not work well with Buffer yet.

S.chain only operates on Right and is a no-op on Left. If parseGif can not parse the Buffer then it returns S.Left (buffer) and the same for parsePng and so forth. swap will inverse that, so the next S.chain will see a Right Buffer and execute the function. If a Buffer can be parsed then the parse function will return Right Image, swap will inverse transform that into Left Image, hence the following S.chain's will be no-op's and the final swap will transform the Either ADT to Right Image if any of the parse functions succeeded. If they all fail, then the return type will be Left Buffer.
This mean that the caller function of parseImage can do a Case analysis for the Either type:

doSomethingUsefulWithAnImage :: Image -> a
IMPURE_displayParseError :: Buffer -> a
S.either (IMPURE_displayParseError) // sad path
         (doSomethingUsefulWithAnImage) // happy path
         (parseImage (buffer))

Case analysis is also something that I feel needs explaining. It's the pattern matching feature of Sanctuary and usually final stop for data transformations.

// Example of sanctuary style pattern match using lower case ADT names
// This example show case how to deal with nested ADT's. Note that
// this kind of pattern matching is destructive, e.i. we will loose the type.
const { maybe, either, pair, show, Nothing, Just, Left, Right, Pair } = S

const patternMatch =
  maybe ('It was Nothing')
        (either (pair (i => s => `It was Left of Pair with ${show (i)} and ${s}`))
                (s => `It was Right with: ${s}`))

console.log (patternMatch (Nothing))
console.log (patternMatch (Just (Left (Pair (42) ('What is the meaning of life, the universe, and everything?')))))
console.log (patternMatch (Just (Right ('Sanctuary'))))

Above was an example provided by @Avaq

@jceb
Copy link
Member

jceb commented Aug 24, 2021

I like the example of parsing JSON, because many people want to do that. However, it lacks the try this, try that style of the parsing images example. Maybe we could combine the two: parse a JSON file, check if an error is included - think JSON API, otherwise return the data.

I'd say let's not define functions with fork in it so that people can directly copy the code and use it in their system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants