4

I have an object of css rules which can have any or none of the following properties:

{ 'font-style': '…',
  'font-variant': '…',
  'font-weight': '…',
  'text-decoration': '…',
  'vertical-align': '…' }

The next step is to build up a css string that is applied to an input, e.g.:

style({'text-decoration': 'underline'}, 'foo');
//=> '<span style="text-decoration:underline">foo</span>'

However if the rules object does not contain any of the above five css rule, the input is returned as is:

style({}, 'foo'); //=> 'foo'

As you can see this is not rocket science but care must be taken so as to not apply an empty css string or include extra stuff that we don't need.

I did come up with a solution using which I was quite happy with until I decided to dive deeper into Monads.

I was impressed with the amount of code I have been able to remove by using a few Monadic principles.

const {curry} = require('ramda');
const {Maybe} = require('monet');

const css = (attrs, key) =>
  attrs[key] ?
    Maybe.of(`${key}:${attrs[key]};`) :
    Maybe.of('');

const style = curry((va, td, fw, fv, fs, input) =>
  va || td || fw || fv || fs ?
    `<span style="${va}${td}${fw}${fv}${fs}">${input}</span>` : input);

module.exports = curry((attrs, input) =>
  Maybe.of(input)
    .ap(css('font-style', attrs)
    .ap(css('font-variant', attrs)
    .ap(css('font-weight', attrs)
    .ap(css('text-decoration', attrs)
    .ap(css('vertical-align', attrs)
    .map(style))))))
    .some());

I'm happy with this but I can't help thinking that all these nested ap are some sort of callbacks hell in disguise. Perhaps there's a better way which I am not aware of?

Question: is there a better way to combine multiple Maybe monads?

customcommander
  • 17,580
  • 5
  • 58
  • 84
  • 1
    This is applicative style here, no monads anywhere. Callback is a term originating from asynchronous computations. `ap` is just a method that has an implicit function wrapped in an applicative context and takes a bunch of Applicatives. There is no callback hell. What you can say is that method chaining avoids nested function composition, that is to say `ap(...).ap(...).ap(...)` is a nested function composition in disguise. Or even better: Method chaining allows for abstracting from nested function composiiton. –  Jun 01 '19 at 11:07
  • 1
    Another way to think of the problem is to treat the Maybe type as either an empty array for `Nothing` or an array with one element for `Some` value. You can then simply `concat` all the array values together. This will give you a similar result to [`catMaybes`](https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Maybe.html#v:catMaybes). – Scott Christopher Jun 01 '19 at 11:54
  • 2
    I've just noticed that you're not actually using anything specific about the `Maybe` type either. All the `Maybe` values produced are using `Maybe.of`, so the use of `.ap` is effectively the same as using something like `Identity` rather than `Maybe`. In other words, your example just ends up as regular function application. `module.exports = attrs => ['font-style', 'font-variant', 'font-weight', 'font-decoration', 'vertical-align'].reduce((fn, k) => fn(css(attrs(k))), style)` – Scott Christopher Jun 01 '19 at 12:08
  • Thanks @ScottChristopher. I see what you mean. I ended up using `Maybe.of` even though I would have preferred leveraging the `Just/Nothing` subtypes. Problem is that `ap` doesn't work with `Nothing`. – customcommander Jun 01 '19 at 20:34
  • @customcommander, does this code actually run? I don't see how `style` is supposed to implement `style({}, input)`. Why does it have 6 parameters? – Mulan Jun 01 '19 at 20:46
  • Sorry my bad. It does run but the first example I gave was just for illustration purpose. The implementation I’m using has six parameters. Each are supplied from a Maybe. – customcommander Jun 01 '19 at 20:52
  • @ScottChristopher Your comment triggered my "aaah" moment. I'm not sure if that was exactly what you had in mind though. I feel this is an improvement (see my answer to my own question). Any thoughts? – customcommander Jun 03 '19 at 07:36
  • ps [tachyons.io](http://tachyons.io/) – Mulan Jul 12 '19 at 00:18

3 Answers3

1

You are really overcomplicating things:

  const keys = ['font-style', 'font-variant', 'font-weight', 'text-decoration', 'vertical-align'];

  const css = attrs => keys
     .map(it => attrs[it] && `${it}: ${attrs[it]}`)
     .filter(it => it)
     .join(", ");

 const style = (attrs, input) =>
   css(attrs) ? `<span style="${css(attrs)}">${input}</span>` : input;
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    That’s a fair answer since I did indeed ask whether that would be better done without monads. Of course it can; thanks for your suggestion. However I don’t want to iterate more than necessary. I’ll edit my question so it’s clearer that I’d like to understand whether there’s a better solution using monads. – customcommander Jun 01 '19 at 10:49
1

This is what I'd do.

const style = (attrs, text) => {
    const props = Object.entries(attrs);
    if (props.length === 0) return text;
    const css = props.map(([key, val]) => key + ":" + val);
    return `<span style="${css.join(";")}">${text}</span>`;
};

const example1 = {};
const example2 = { "text-decoration": "underline" };
const example3 = { "font-weight": "bold", "font-style":  "italic" };

console.log(style(example1, "foo")); // foo
console.log(style(example2, "foo")); // <span style="text-decoration:underline">foo</span>
console.log(style(example3, "foo")); // <span style="font-weight:bold;font-style:italic;">foo</span>

Note that although this might look like imperative code, it is actually purely functional. It can be transliterated to Haskell as follows.

import Data.List (intercalate)
import Data.Map.Strict (fromList, toList)

style (attrs, text) =
    let props = toList attrs in
    if length props == 0 then text else
    let css = map (\(key, val) -> key ++ ":" ++ val) props in
    "<span style=\"" ++ intercalate ";" css ++ "\">" ++ text ++ "</span>"

example1 = fromList []
example2 = fromList [("text-decoration", "underline")]
example3 = fromList [("font-weight", "bold"), ("font-style", "italic")]

main = do
    putStrLn $ style (example1, "foo") -- foo
    putStrLn $ style (example2, "foo") -- <span style="text-decoration:underline">foo</span>
    putStrLn $ style (example3, "foo") -- <span style="font-weight:bold;font-style:italic;">foo</span>

Note that it's not recommended to shoehorn monads and composition into every functional program.

Functional programming is about more than just monads and composition. At its heart its all about transforming inputs into outputs without side effects. Monads and composition are only some of the tools that functional programming provides to do that. There are more tools in the box. You just need to find the right tool.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
0

As pointed out I don't think I was leveraging the Maybe type as much as I could.

I finally settled on the following solution:

  1. I accept the initial rules object as is
  2. Later on (cf chain) I decide whether that object is something that I can use
  3. I carry on with regular mapping functions
  4. Finally if I had Nothing I return the input as is, otherwise I apply the computed css string
const styles = (rules, input) =>
  Maybe
    .of(rules)
    .map(pick(['font-style', 'font-variant', 'font-weight', 'text-decoration', 'vertical-align']))
    .chain(ifElse(isEmpty, Maybe.none, Maybe.some))
    .map(toPairs)
    .map(reduce((str, arr) => str + arr.join(':') + ';', ''))
    .fold(input)(css => `<span style="${css}">${input}</span>`);



styles({}, 'foo');
//=> 'foo'

styles({'text-decoration':'underline'}, 'foo');
//=> '<span style="text-decoration:underline;">foo</span>'
customcommander
  • 17,580
  • 5
  • 58
  • 84
  • I also submitted this issue to [monet.js](https://github.com/monet/monet.js/issues/213) to create a new static helper which will help with this use case. – customcommander Jun 03 '19 at 10:17