2

I am following the first half of this excellent article, but there is a place I am stuck. https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/

I have implemented a very similar Maybe monad, but one of my functions I need to pass to map is asynchronous. Ideally, I would be able to do this in a combination of .then() and map(). I want to do something like this...

const getToken = async (p) => {
    let result = utils.Maybe.of(await makeAICCCall(p.aiccsid, p.aiccurl))
                    .map(parseAuthenticatedUser)
                    .thenMap(syncUserWithCore) <-- I can't figure this out
                    .map(managejwt.maketoken)
                    .value

    return result;
}

I have tried everything I can think of, but I have not been able to figure this out.

Danny Ellis Jr.
  • 1,674
  • 2
  • 23
  • 38
  • It saddens me to see you struggle like this -_- – Mulan Oct 26 '17 at 15:55
  • @naomik. Your sympathies are appreciated. :) Just FYI, were it not for the asynchronous function, I have it doing exactly what I want it to do. I will play around with your new answer and see where it gets me. Looks promising. – Danny Ellis Jr. Oct 26 '17 at 16:04
  • Sounds good, Danny. Note, I had `resolve` and `reject` switcherooed in my `eitherToPromise` – I fixed it and provided a working code snippet ^_^ – Mulan Oct 26 '17 at 16:19
  • You appear to have a Promise with a Maybe inside, not the other way round. – Bergi Oct 26 '17 at 17:39
  • My question was just about this: .thenMap(syncUserWithCore) <-- I can't figure this out. syncUserWithCore returns a Promise which doesn't work in the map. – Danny Ellis Jr. Oct 26 '17 at 17:44
  • I should say also .thenMap() is something I made up to attempt to describe what I would like it to do. – Danny Ellis Jr. Oct 26 '17 at 17:45

1 Answers1

1

natural transformations

Nesting of data containers can get messy, but there's a well-known technique for keeping them flat – eitherToPromise below is considered a natural transformation – it converts an Either to a Promise which allows it to be flattened in the .then chain, but also simultaneously prevents your value/error wires from getting crossed

Note: You probably want to use makeAICCall to return an Either (Left, Right) instead of Maybe because you'll be able to return an error message (instead of Nothing, which is less informative)

import { Left, Right, eitherToPromise } from './Either'

const makeAICCall = (...) =>
  someCondition
    ? Left (Error ('error happened'))
    : Right (someResult)

const getToken = p =>
  makeAICCall (p.aiccsic, p.aiccurl) // => Promise<Either<x>>
    .then (eitherToPromise)          // => Promise<Promise<x>>
                                     // => auto-flattened to Promise<x>
    .then (syncUserWithCore)         // => Promise<x>
    .then (managejwt.maketoken)      // => Promise<x>

Supply your favourite implementation of Either

// Either.js

export const Left = x =>
  ({
    fold: (f,_) => f (x),
    // map: f => Left (x),
    // chain: ...,
    // ...
  })

export const Right = x =>
  ({
    fold: (_,f) => f (x),
    // map: f => Right (f (x)),
    // chain: ...,
    // ...
  })

export const eitherToPromise = m =>
  m.fold (x => Promise.reject (x), x => Promise.resolve (x))

runnable demo

const someAsyncCall = x =>
  new Promise (r => setTimeout (r, 1000, x))

const authenticate = ({user, password}) =>
  password !== 'password1'
    ? Left (Error ('invalid password'))
    : Right ({user, id: 123})

const someSyncCall = token =>
  Object.assign (token, { now: Date.now () })

const getToken = x =>
  someAsyncCall (x)
    .then (authenticate)
    .then (eitherToPromise)
    .then (someSyncCall)

// minimal dependencies
const Left = x =>
  ({ fold: (f,_) => f (x) })

const Right = x =>
  ({ fold: (_,f) => f (x) })

const eitherToPromise = m =>
  m.fold (x => Promise.reject (x), x => Promise.resolve (x))

// test it
getToken ({user: 'alice', password: 'password1'})
  .then (console.log, console.error)
  // 1 second later ...
  // { user: 'alice', id: 123, now: 1509034652179 }

getToken ({user: 'bob', password: 'password2'})
  .then (console.log, console.error)
  // 1 second later ...
  // Error: invalid password ...

hey, lookit that

Our solution above results in a sequence of .then calls – an answer to your previous question demonstrates how such a program can be expressed in a different way

nullables

You should try your best to write functions that have a well-defined domain and codomain – you should be able to say, for example,

My function takes a string (domain) and returns a number (codomain) – anonymous wiseman

And avoid writing functions that have descriptions like,

It can take a number or a string and it returns an array, but could also return undefined. Oh and sometimes it can throw an error. But that's it, I'm pretty sure. – anonymous ignoramus

But of course we'll be dealing with null and undefined sometimes. How can we deal with it in a "functional way" – that's what you're wondering, right?

If you find yourself in an encounter with a function's nullable codomain (ie, can return a nullable), we can create a little helper to coerce it into a type we want. We'll demonstrate again with Either, just to tie it into the original code later

const Left = x =>
  ({ fold: (f,_) => f (x) })

const Right = x =>
  ({ fold: (_,f) => f (x) })
  
const eitherFromNullable = (x, otherwise = x) =>
  x === null ||
  x === undefined 
    ? Left (otherwise)
    : Right (x)

// !! nullable codomain !!
const find = (f, xs) =>
  xs.find (x => f (x))    

// example data
const data =
  [1,2,3,4,5]

// perform safe lookups by wrapping unsafe find in eitherFromNullable
eitherFromNullable (find (x => x > 3, data))
  .fold (console.error, console.log)
  // <console.log> 4
  
eitherFromNullable (find (x => x > 5, data))
  .fold (console.error, console.log)
  // <console.error> undefined
  
eitherFromNullable (find (x => x > 5, data), Error (`couldn't find a big number !`))
  .fold (console.error, console.log)
  // <console.error> Error: couldn't find a big number !
  

nullables and natural transformations

Remember, we do our best to avoid nullables, but sometimes we can't help it. To show how this might tie in with the original code, let's pretend that instead of returning an Either, makeAICCall will instead return some x or some null

We just screen it with eitherFromNullable – new code in bold

const getToken = p =>
  makeAICCall (p.aiccsic, p.aiccurl) // =&gt Promise<x?> could be null !!
    .then (x =>                      // =&gt Promise<Either<x>>
      eitherFromNullable (x, Error ('bad aic call')))
    .then (eitherToPromise)          // =&gt Promise<Promise<x>>
                                     // =&gt auto-flattened to Promise<x>
    .then (syncUserWithCore)         // =&gt Promise<x>
    .then (managejwt.maketoken)      // =&gt Promise<x>

stop hating lambdas

You probably wanna ditch that lambda, right ? Ok fine, just don't make it into a fetish.

const eitherFromNullable = otherwise => x =>
  x == null ? Left (otherwise) : Right (x)

// ooooh, yeah, you like that
makeAICCall (p.aiccsic, p.aiccurl)
  .then (eitherFromNullable (Error ('bad aic call')))
  .then (eitherToPromise)
  .then ...

don't get stuck

Nullable doesn't mean anything – you decide what it means in the context of your program.

const eitherFromNullable = (x, otherwise = x) =>
  // we consider null and undefined nullable,
  // everything else is non-null
  x === null ||
  x === undefined 
    ? Left (otherwise)
    : Right (x)

You could decide that false and 0 and empty string '' are also "nullable" – Or, you could just as easily decide to have very specific adapters eitherFromNull, eitherFromUndefined, eitherFromBoolean, etc – it's your program; it's up to you!

I feel like I'm starting to repeat myself ^_^'

make it routine

So you're saying you have lots of areas in your program where nulls are just unavoidable; maybe it's some dependency that you cannot get rid of. We'll imagine your code base with the following

// no one wants to do this for every endpoint!
const getUser = id =>
  new Promise ((resolve, reject) =>
    request ({url: '/users', id}, (err, res) =>
      err
        ? reject (err)
        : res.status === 403
          ? reject (Error ('unauthorized'))
          res.body == null ?
            ? reject (Error ('not found'))
          : resolve (User (JSON.parse (res.body)))))
  • It's using request which has an older Node-style callback interface that we're tired of wrapping in a promise
  • The API endpoints will respond with a 403 status if the requestor is unauthorized to view the requested resource
  • The API endpoints we talk to sometimes respond with null with status 200 (instead of 404) for missing resources; for example /users/999, where 999 is an unknown user id, will not trigger an error, but will fetch an empty response body
  • The API will respond with a valid JSON document for all other requests

We wish we could use something other than request, but our supervisor says No. We wish the API endpoints had different behavior, but that's out of our control. Still, it's within our power to write a good program

// functional programming is about functions
const safeRequest = (type, ...args) =>
  new Promise ((resolve, reject) =>
    request[type] (args, (err, res) =>
      err
        ? reject (err)
        : res.status === 403
          ? reject (Error ('unauthorized'))
          res.body == null ?
            ? reject (Error ('not found'))
          : resolve (JSON.parse (res.body))))

const getUser = id =>
  safeRequest ('get', {url: '/users', id})

const createUser = fields =>
  safeRequest ('post', {url: '/users', fields})

const updateUser = (id, fields) =>
  safeRequest ('put', {url: '/users', id, fields})

Can it be improved more? Sure, but even if that's as far as you went, there's nothing wrong with that; all of the necessary checks happen for each endpoint because they were defined using safeRequest

Ok, so you wanna take it further? No problem. It's your program, do whatever you want!

const promisify = f => (...args) =>
  new Promise ((resolve, reject) =>
    f (...args, (err, x) =>
      err ? reject (err) : resolve (x)))

const promiseFromResponseStatus = res =>
  res.status === 403 // or handle other status codes here too !
    ? Promise.reject (Error ('unauthorized'))
    : Promise.resolve (res)

const promiseFromNullableResponse = res =>
  res.body == null // or res.body == '', etc
    ? Promise.reject (Error ('not found'))
    : Promise.resolve (res.body)

const safeRequest = (type, ...args) =>
  promisify (request [type]) (...args)
    .then (promiseFromResponseStatus)
    .then (promiseFromNullableResponse)
    .then (JSON.parse)

const getUser = id =>
  safeRequest ('get', {url: '/users', id})

const createUser ...

....
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Okay, I have been looking this over and this is a better way to do what I'm doing "inside" the functions I'm calling within my maps(). However, this does not do for me what my Maybe implementation does. Inside my maybe implementation, I am checking for: null, undefined, empty strings, empty objects, and a specific error object that I generate in some of the functions. I don't want to do all that checking in every function, and of course, the Maybe is reusable. I could possibly see overriding .then() in some way, but I don't want every Promise's .then() to do all of this. Make sense? – Danny Ellis Jr. Oct 26 '17 at 17:26
  • Your answer works! Well...sort of... But, I end up having to put *.then(x => monads.eitherFromNullable(x))*, and *.then(monads.eitherToPromise)* between every one of my real business function calls. Triple the calls? Really? Are you making fun of me with this answer? Why not just write a big ole' serial function with each one async / await'ed assigned to variables and then just null and errors checks in between in the good ole' procedural way? At least my fellow developers, who know less about Function Programming than I do, could understand that. This is disappointing. – Danny Ellis Jr. Oct 27 '17 at 13:12
  • If you group your code in `utils` or `monads` that’s your own problem - how about `promiseFromNullable`? And anyway, I told you nullables are your problem, stop trying to fix everything *around* them. If you say you will *always* need to do null checks, you’re way ahead of yourself fussing about monads – Mulan Oct 27 '17 at 13:37
  • Who cares if a thing takes 3 steps or 30? How many steps in a heart surgery? How many steps in an HTTPS request? Doesn’t matter; we can just make it a function (procedure) and repeat it as necessary. We don’t skip steps just because you don’t want to do more work. Use your functions! Have I taught you anything? – Mulan Oct 27 '17 at 13:44
  • I have no idea what point you are making about how I group my functions. They obviously don't belong on every one of my 100+ API end-point files, so... To your other point, there's a difference between "business" steps and "boilerplate" steps. Is one of the steps in heart surgery for the nurse to mop sweat off the doctor's brow? No, it is not. That is incidental to the process. The whole reason for doing this at all is to abstract the boilerplate steps away, to define an approach that saves other devs time and thought. You solution achieves none of this. – Danny Ellis Jr. Oct 27 '17 at 14:15
  • You have taught me quite a bit, and I am grateful. However, all through this exchange, I have thought you sound more like an academic than a working developer. Many of your remarks and suggestions clearly reflect an absence of understanding of the complexities of a real domain. – Danny Ellis Jr. Oct 27 '17 at 14:19
  • For example, any suggestion that it is a problem that I need to check for nulls in every function shows that you have an overly simplistic view of the domain. One of the functions I've written is called from an incoming request. I have no way of controlling whether or not I'm POSTed usable data. Two of the 4 functions I need to call make REST calls to external systems that I have no control over. – Danny Ellis Jr. Oct 27 '17 at 14:26
  • Again, thank you for your responses. They definitely made me think differently about the problem. – Danny Ellis Jr. Oct 27 '17 at 14:27
  • Danny, I sense you have perceived me in a way that will make it difficult for us to make progress. I've made a final edit but will let the matter come to rest – If you need help in the future, feel free to approach me. Cheers ^_^ – Mulan Oct 27 '17 at 18:18