kibe
kibe

Reputation: 303

How to compose/pipe functions when dealing with objects?

Say I have a Request object:

{
  user: { /* user data: username, email, etc. */ }
  post: { /* post data: content, date, etc. */ }
}

Example of a Request object:

{
  user: {
    id: '123'
    username: 'kibe'
    email: '[email protected]'
  }
  post: {
    content: 'my new post!'
    date: '20/02/2004'
  }
}

Now, I have two functions: validateUser and validatePost. Both of them return a Maybe monad, because they might fail.

How can I then do something like this?

function savePost (request) {
  return request
  |> validateUser // if validateUser returns either Success(user) or Failure(error), how would I pass down the post?
  |> validatePost
  |> savePostToDb
  |> ok
}

Should I create a function validateRequest which composes validateUser and validatePost? But then, how would I only give the post object to savePostToDb? What if savePostToDb also requires the user ID?

function savePost (request) {
  return request
  |> validateRequest // returns an Either monad
  |> savePostToDb // only requires user ID and the post, how would I pass these down? 
  |> ok
}

Hopefully these questions make sense. I am new to FP and although I am understanding its paradigms, I am failing to design a simple program

Thanks!

Upvotes: 0

Views: 581

Answers (2)

Mulan
Mulan

Reputation: 135377

prop

We write a simple prop function which safely looks up properties on your object. If they are present, we get a Just-wrapped value, otherwise we get Nothing -

const fromNullable = x =>
  x == null
    ? Nothing
    : Just(x)

const prop = k => t =>
  fromNullable(t[k])

props

You have nested properties though, so let's make it so we can dig arbitrarily deep into your object using any sequence of props -

const props = (k, ...ks) => t =>
  ks.length
    ? prop(k)(t).bind(props(...ks))
    : prop(k)(t)

validate

Now we can write a simple validate coroutine -

function* validate (t)
{ const id = yield props("user", "id")(t)
  const username = yield props("user", "username")(t)
  const email = yield props("user", "email")(t)
  const post = yield props("post", "content")(t)
  const date = yield props("post", "date")(t)
  return Just({ id, username, email, post, date }) // <- Just(whateverYouWant)
}

const doc = 
  { user:
      { id: '123'
      , username: 'kibe'
      , email: '[email protected]'
      }
  , post:
      { content: 'my new post!'
      , date: '20/02/2004'
      }
  }

coroutine(validate(doc)).bind(console.log)

With a valid doc we see -

{
  id: '123',
  username: 'kibe',
  email: '[email protected]',
  post: 'my new post!',
  date: '20/02/2004'
}

With an invalid document, otherdoc -

const otherdoc = 
  { user:
      { id: '123'
      , username: 'kibe'
      ,                           // <- missing email
      }
  , post:
      { content: 'my new post!'
      ,                           // <- missing date
      }
  }

coroutine(validate(otherdoc)).bind(console.log)
// no effect because validate returns a Nothing!

coroutine

Implementation of coroutine is simple and most importantly it is generic for any monad -

function coroutine (f)
{ function next (v)
  { let {done, value} = f.next(v)
    return done ? value : value.bind(next)
  }
  return next()
}

Maybe

Lastly we supply implementation for Nothing and Just -

const Nothing = 
  ({ bind: _ => Nothing })
  
const Just = v =>
  ({ bind: f => f(v) })

demo

Expand the snippet below to verify the results in your own browser -

const Nothing = 
  ({ bind: _ => Nothing })
  
const Just = v =>
  ({ bind: f => f(v) })
 
const fromNullable = x =>
  x == null
    ? Nothing
    : Just(x)

const prop = k => t =>
  fromNullable(t[k])
  
const props = (k, ...ks) => t =>
  ks.length
    ? prop(k)(t).bind(props(...ks))
    : prop(k)(t)
 
function coroutine (f)
{ function next (v)
  { let {done, value} = f.next(v)
    return done ? value : value.bind(next)
  }
  return next()
}

function* validate (t)
{ const id = yield props("user", "id")(t)
  const username = yield props("user", "username")(t)
  const email = yield props("user", "email")(t)
  const post = yield props("post", "content")(t)
  const date = yield props("post", "date")(t)
  return Just({ id, username, email, post, date })
}

const doc =
  {user:{id:'123',username:'kibe',email:'[email protected]'},post:{content:'my new post!',date:'20/02/2004'}}

coroutine(validate(doc)).bind(console.log)

related reading

Upvotes: 1

Be Br
Be Br

Reputation: 61

Yes you should create validateRequest and a savePostToDb Methodes which give you a boolean back. Then you can simple create two Maybe-Functions with thos two new methodes and compose it togheter. Look at my example how I would do it simple:

// Here some simple Left/Right to Just and Nothing (Maybe)-Monade function to work with
const Left    = x => f => _ => f(x);
const Right   = x => _ => g => g(x);

const Nothing = Left();
const Just    = Right;

// your maybe-validateRequest function
const maybeValidateRequest  = request =>
    validateRequest(request)
        ? Just(user)
        : Nothing

// your maybe-postToDb function
const maybePostToDb = request =>
    savePostToDb(request)
        ? Just(request)
        : Nothing

// and finally compose those two maybes and do the postRequest finally
const savePost = request =>
    maybeValidateRequest(request)
    (() => console.error("Validation failed"))
    (_  =>
        maybePostToDb(request)
        (() => console.error("Save to DB failed")))
        (_  => postRequest(request))    // when it's everything passed,the postRequest can be safely made

Upvotes: 0

Related Questions