4

Given an object that may be null and may have the following properties:

{
  templateId: "template1",
  templates: {
    template1: "hello"
  }
}

How would you get the template in a failsafe way? (templateId might not be defined, or the template it reffers might be undefined)

I use ramda and was trying to adapt my naive version of the code to use something like a Maybe adt to avoid explicit null/undefined checks.

I'm failing to come up with an elegant and clean solution.

naive ramda version:

const getTemplate = obj => {
  const templateId = obj && prop("templateId", obj);
  const template = templateId != null && path(["template", templateId], obj);
  return template;
}

this does work but I would like to avoid null checks, as my code has a lot more going on and it would be really nice to become cleaner

Edit I get from severall answers that the best is to ensure clean data first. That's not allways possible though. I also came up with this, which I do like.

const Empty=Symbol("Empty"); 
const p = R.propOr(Empty);
const getTemplate = R.converge(p,[p("templateId"), p("templates")]);

Would like to get feedback regarding how clean and how readable it is (and if there are edge cases that would wreck it)

Tiago Coelho
  • 5,023
  • 10
  • 17
  • 4
    “Given an object that may be null and may have the following properties” rules out the possibility of a “clean” solution. :P – davidchambers Apr 23 '18 at 13:29
  • 2
    Everyone is pointing to how your input data is the issue. They're right. But I'm often in the position of dealing with such data, from legacy systems, from inconsistent APIs offered by third-party vendors long ago chosen by a vast enterprise, from junior developers on another team who don't understand how to use proper error responses or empty values, from all sorts of places. Your goal will be to work with this. If you have such data, perhaps your own first step would be to clear it up. If you start with `map(defaultTo({templates: []}))`, you will eliminate several of your `nil` checks. – Scott Sauyet Apr 23 '18 at 14:34
  • 1
    Sorry, that `map` was from my own solution to the problem, not worth posting. I used a list of values as in the solution from user633183. Depending on your solution, you might just use the `defaultTo` part of it. – Scott Sauyet Apr 23 '18 at 14:52
  • I get that @davidchambers, @scott-sauyet, @user633183 thanks for your answers, and for taking the time!! I like this kind of solution: `const Empty=Symbol("Empty"); const p = R.propOr(Empty);const getTemplate = R.converge(p,[p("templateId"), p("templates")]);` but would really like to have the input of such experienced and clever guys like you, as I feel it is a little hard to read. – Tiago Coelho Apr 23 '18 at 14:56

4 Answers4

4

Here is an ADT approach in vanilla Javascript:

// type constructor

const Type = name => {
  const Type = tag => Dcons => {
    const t = new Tcons();
    t[`run${name}`] = Dcons;
    t.tag = tag;
    return t;
  };

  const Tcons = Function(`return function ${name}() {}`) ();
  return Type;  
};

const Maybe = Type("Maybe");

// data constructor

const Just = x =>
  Maybe("Just") (cases => cases.Just(x));

const Nothing =
  Maybe("Nothing") (cases => cases.Nothing);

// typeclass functions

Maybe.fromNullable = x =>
  x === null
    ? Nothing
    : Just(x);

Maybe.map = f => tx =>
  tx.runMaybe({Just: x => Just(f(x)), Nothing});

Maybe.chain = ft => tx =>
  tx.runMaybe({Just: x => ft(x), Nothing});

Maybe.compk = ft => gt => x => 
  gt(x).runMaybe({Just: y => ft(y), Nothing});

// property access

const prop =
  k => o => o[k];

const propSafe = k => o =>
  k in o
    ? Just(o[k])
    : Nothing;

// auxiliary function

const id = x => x;

// test data

// case 1

const o = {
  templateId: "template1",
  templates: {
    template1: "hello"
  }
};

// case 2

const p = {
  templateId: null
};

// case 3

const q = {};

// case 4

const r = null; // ignored

// define the action (a function with a side effect)

const getTemplate = o => {
  const tx = Maybe.compk(Maybe.fromNullable)
    (propSafe("templateId"))
      (o);

  return Maybe.map(x => prop(x) (o.templates)) (tx);
};

/* run the effect,
   that is what it means to compose functions that may not produce a value */


console.log("case 1:",
  getTemplate(o).runMaybe({Just: id, Nothing: "N/A"})
);

console.log("case 2:",
  getTemplate(p).runMaybe({Just: id, Nothing: "N/A"})
);

console.log("case 3:",
  getTemplate(q).runMaybe({Just: id, Nothing: "N/A"})
);

As you can see I use functions to encode ADTs, since Javascript doesn't support them on the language level. This encoding is called Church/Scott encoding. Scott encoding is immutable by design and once you are familiar with it, its handling is a piece of cake.

Both Just values and Nothing are of type Maybe and include a tag property on which you can do pattern matching.

[EDIT]

Since Scott (not the encoding guy from just now) and the OP asked for a more detailed reply I extended my code. I still ignore the case where the object itself is null. You have to take care of this in a preceding step.

You may think this is overengineered - with certainty for this contrived example. But when complexity grows, these functional style can ease the pain. Please also note that we can handle all kinds of effects with this approach, not just null checks.

I am currently building an FRP solution, for instance, which is essentially based on the same building blocks. This repetition of patterns is one of the traits of the functional paradigm I would not want to do without anymore.

  • can you extend it a bit to actually get the template, like my original code? (I gave you a vote for teaching me something new) – Tiago Coelho Apr 23 '18 at 13:30
  • This approach only applies for the `templateId` property, which may be `null`. If the object itself may be `null` the computation that produces this object must return a `Maybe` as well. –  Apr 23 '18 at 13:41
  • yes, the point is how do you compose this... What I do in my original code is to get the templateId, and then get the corresponding template. your answer is mostly the same as using ramda's propOr only. propOr("N/A","templateId", o) – Tiago Coelho Apr 23 '18 at 13:46
  • You can simply define `map`, `ap` and `chain` instances to compose pure functions or chain actions. They are straightforward for the `Maybe` ADT. In this respect my approach is more expressive than Ramda's, because it relies on types and hence can rely on overloaded functions. –  Apr 23 '18 at 13:54
  • 1
    @ftor: I too would love to see this completed to solve the original problem. This is very nice. – Scott Sauyet Apr 23 '18 at 15:31
  • Of course it's overengineered for a simple problem, but it demonstrates how to grow a solution for this one into something more general. Again, a very nice solution! – Scott Sauyet Apr 23 '18 at 21:37
4

As others have told you, ugly data precludes beautiful code. Clean up your nulls or represent them as option types.

That said, ES6 does allow you to handle this with some heavy destructuring assignment

const EmptyTemplate =
  Symbol ()

const getTemplate = ({ templateId, templates: { [templateId]: x = EmptyTemplate } }) =>
  x
  
console.log
  ( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
  , getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
  , getTemplate ({                  templates: { a: "hello" }}) // EmptyTemplate
  )

You can continue to make getTemplate even more defensive. For example, below we accept calling our function with an empty object, and even no input at all

const EmptyTemplate =
  Symbol ()

const getTemplate =
  ( { templateId
    , templates: { [templateId]: x = EmptyTemplate } = {}
    }
  = {}
  ) =>
    x
 
console.log
  ( getTemplate ({ templateId: "a", templates: { a: "hello" }}) // "hello"
  , getTemplate ({ templateId: "b", templates: { a: "hello" }}) // EmptyTemplate
  , getTemplate ({                  templates: { a: "hello" }}) // EmptyTemplate
  , getTemplate ({})                                            // EmptyTemplate
  , getTemplate ()                                              // EmptyTemplate
  )

Above, we start to experience a little pain. This signal is important not to ignore as it warns us we're doing something wrong. If you have to support that many null checks, it indicates you need to tighten down the code in other areas of your program. It'd be unwise to copy/paste any one of these answers verbatim and miss the lesson everyone is trying to teach you.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Good one. although it won't work with a null obj. and will also give a weird output if it happens to get an object like this: {templates:{undefined:1}} (I'm voting up for the nice use of destructuring) – Tiago Coelho Apr 23 '18 at 13:53
  • @Tiago, I updated my answer. Do you see how each null check compounds the problem? Everyone here is trying to teach you a valuable lesson - the problem starts with your input. – Mulan Apr 23 '18 at 14:05
  • @TiagoCoelho ie, if you walk away from this thinking *"nice use of destructuring"*, you indeed missed the lesson. – Mulan Apr 23 '18 at 14:19
  • data can be unclean though... If I understand correctly you are telling me I need to filter the data. I can relate to that, and I agree. I think you should not assume this indicates any bad code in other parts of my program though. I know each null check compounds the problem... that's the one reason I'm asking for input here. I was hoping to see some use of the Maybe data types(or similar) that would accomodate those requirements nicely. – Tiago Coelho Apr 23 '18 at 14:30
3

You can use R.pathOr. Whenever any part of the path isn't available, a default value is returned. For example:

const EmptyTemplate = Symbol();

const getTemplateOrDefault = obj => R.pathOr(
  EmptyTemplate,
  [ "templates", obj.templateId ],
  obj
);

A collection of tests can be found in this snippet. The example shows that pathOr handles all (?) "wrong" cases quite well:

const tests = [
  { templateId: "a",  templates: { "a": 1 } }, // 1
  {                   templates: { "a": 1 } }, // "empty"
  { templateId: "b",  templates: { "a": 1 } }, // "empty"
  { templateId: null, templates: { "a": 1 } }, // "empty"
  { templateId: "a",  templates: {        } }, // "empty"
  { templateId: "a"                         }  // "empty"
];

Edit: To support null or undefined inputs, you could compose the method with a quick defaultTo:

const templateGetter = compose(
  obj => pathOr("empty", [ "templates", obj.templateId ], obj),
  defaultTo({})
);
user3297291
  • 22,592
  • 4
  • 29
  • 45
0

Try this,

const input = {
  templateId: "template1",
  templates: {
    template1: "hello"
  }
};

const getTemplate = (obj) => {
    const template = obj.templates[obj.templateId] || "any default value / simply remove this or part";
    //use below one if you think templates might be undefined too,
    //const template = obj.templates && obj.templates[obj.templateId] || "default value"
    return template;
}
console.log(getTemplate(input));

You can use a combination of && and || to short-circuit an expression.

Also, use [] (instead of .) with objects to get the value if the key is stored in a variable.

Complete check

const getTemplate = (obj) => {
    const template = obj && obj.templateId && obj.templates && obj.templates[obj.templateId] || "default value"
    return template;
}
Satish Kumar
  • 601
  • 6
  • 14
  • this will not work for null obj, and also won't work for an object like this: {templates:{undefined:1},a:2} – Tiago Coelho Apr 23 '18 at 12:54
  • @TiagoCoelho. Like I said, we can use the combination of && and || to check for conditions when a variable is undefined/null. So for null obj, we can do, obj && obj.templates[obj.templateId] || "default value". And for the example you gave, What should be the expected output? Since templateId is undefined, above snippet will output as 1. If 1 is not correct answer then you may do, something like obj && obj.templates && obj.templateId && obj.templates[obj.templateId] || "default value". I wil update my answer to reflect this. – Satish Kumar Apr 23 '18 at 13:32
  • thank you for answering. My point was that I'm asking about an elegant and clean solution without the explicit checks, adding these checks makes the code intent fade way. My version will return undefined when "templateId" is undefined, that's what I would expect also. (or some default / "N/A") – Tiago Coelho Apr 23 '18 at 13:43