Peter Kellner
Peter Kellner

Reputation: 15498

Looking for a clean function implementation of a switch statement in JavaScript

I am teaching a course that includes explaining functional JavaScript and I want to have a really good example of functional programming that is hopefully cleaner then non-functional. I want to convert the following switch statement to functional. I've made an example of that conversion myself, but hoping there is a simpler solution.

Here is the switch statement version:

let animalType = "Poodle";
switch (animalType) {
  case "Poodle":
  case "Beagle":
  case "Bulldog":
    console.log(animalType + " is a dog.");
    break;
  case "Bengal":
  case "Siamese":
    console.log(animalType + " is a cat.");
    break;
  default:
    console.log(animalType + " is not a dog or cat.");
    break;
}

And here is what I came up with as functional that I'm not that happy about

const result = getAnimalType("Poodle");
console.log("result:" + result)

function getAnimalType(animal) {
  function isDog(animal) {
    const dogs = ["Poodle", "Beagle", "Bulldog"];
    return dogs.includes(animal)
  }
  function isCat(animal) {
    const cats = ["Bengal", "Siamese"];
    return cats.includes(animal)
  }
  return isDog(animal)
    ? animal + " is a dog."
    : isCat(animal)
    ? animal + " is a cat."
    : animal + " is not a dog or cat.";
}

Upvotes: 0

Views: 1228

Answers (7)

customcommander
customcommander

Reputation: 18921

Working with maps would be my preferred option in this case. The question is how. I like curried functions so I'll use those:

const animalType =
  (type => animal =>
    type[animal]
      ? `${animal} is a ${type[animal]}`
      : `${animal} is neither a dog nor a cat`)
    ({ Poodle:  'dog'
     , Beagle:  'dog'
     , Bulldog: 'dog'
     , Bengal:  'cat'
     , Siamese: 'cat'});

animalType('Poodle');
//> 'Poddle is a dog'

What's going on here?

We have a function that takes a map then returns a function that finally takes an animal and returns a message. The key is to realise that we also use an IIFE:

const animalType =
  (type => animal => {/*…*/})({Poodle: 'dog'});
// ^^^^                       ^^^^^^^^^^^^^^^
// |                          |
// +--------------------------+

The function animalType is "hardcoded" to work with the type we gave it. Now it just waits for the animal parameter to come in.

However this seems like something we could abstract and reuse:

const lookup =
  (pass, fail, map) => key =>
    key in map
      ? pass(key, map[key])
      : fail(key);

The lookup function takes a success function pass and a failure function fail and an initial map. Then it returns a function that takes a key and applies pass to key and the corresponding value if the key exists in the map. Otherwise it applies fail to the key.

With that we can build animalType this way:

const animalType =
  lookup( (key, val) => `${key} is a ${val}`
        , (key) => `${key} is neither a dog nor a cat`
        , { Poodle:  'dog'
          , Beagle:  'dog'
          , Bulldog: 'dog'
          , Bengal:  'cat'
          , Siamese: 'cat'});

animalType('Poodle');
//> 'Poddle is a dog'

Upvotes: 1

ManuelMB
ManuelMB

Reputation: 1375

I get something similar to this by compiling a TypeScript enum code example:

var AnimalType;
(function (AnimalType) {
    AnimalType[AnimalType["Poodle"] = 0] = "Poodle";
    AnimalType[AnimalType["Beagle"] = 1] = "Beagle";
    AnimalType[AnimalType["Bulldog"] = 2] = "Bulldog";
    AnimalType[AnimalType["Bengal"] = 3] = "Bengal";
    AnimalType[AnimalType["Siamese"] = 4] = "Siamese";
})(AnimalType || (AnimalType = {}));

function whichKind(animalType) {
    let kind;
    switch (animalType) {
        case AnimalType.Poodle:
            kind = `${AnimalType[animalType]} is a dog`;
            break;
        case AnimalType.Beagle:
            kind = `${AnimalType[animalType]} is a dog`;
            break;
        case AnimalType.Bulldog:
            kind= `${AnimalType[animalType]} is a dog`;
            break;
       case AnimalType.Bengal:
            kind = `${AnimalType[animalType]} is a cat`;
            break;
       case AnimalType.Siamese:
            kind = `${AnimalType[animalType]} is a cat`;
           break;
    }
 
    kind = typeof kind === 'undefined' ? `${animalType} is not a dog or cat.` : kind;

    return kind;
}

console.log(whichKind(AnimalType.Poodle)); // Poodle is a dog

// console.log(whichKind('Other')); // Other is not a dog or cat.

Upvotes: 0

Bergi
Bergi

Reputation: 664936

There's nothing wrong with switch from the perspective of functional programming. The problem rather is that it calls console.log as a side effect, instead of returning a value. Easy to fix, though:

function getAnimalType(animalType) {
  switch (animalType) {
    case "Poodle":
    case "Beagle":
    case "Bulldog":
      return animalType + " is a dog.";
    case "Bengal":
    case "Siamese":
      return animalType + " is a cat.";
    default:
      return animalType + " is not a dog or cat.";
  }
}

console.log(getAnimalType("Poodle"));

Next improvement might be avoiding some duplication:

function getAnimalType(animalType) {
  switch (animalType) {
    case "Poodle":
    case "Beagle":
    case "Bulldog":
      return "a dog";
    case "Bengal":
    case "Siamese":
      return "a cat";
    default:
      return "not a dog or cat";
  }
}
function getStatement(animalType) {
  return animalType + " is " + getAnimalType(animalType) + ".";
}
console.log(getStatement("Poodle"));

Upvotes: 1

Mister Jojo
Mister Jojo

Reputation: 22335

a variant, a bit like Barmar's, but which remains personal to me

const getAnimalType = (() => 
  {
  const
    isX =
    { dog : 'is a dog'
    , cat : 'is a cat'
    , nDC : 'is neither a dog nor a cat'
    }
  , typeMap = 
    { Poodle  : 'dog'
    , Beagle  : 'dog'
    , Bulldog : 'dog'
    , Bengal  : 'cat'
    , Siamese : 'cat'
    };
  return (animal) => `${animal} ${isX[ typeMap[animal] ?? 'nDC']}`
  })()

console.log(getAnimalType('Beagle'))
console.log(getAnimalType('Bengal'))
console.log(getAnimalType('schtroumpf'))
.as-console-wrapper {max-height: 100% !important;top: 0;}
.as-console-row::after {display: none !important;}

Upvotes: 1

Ashirbad Panigrahi
Ashirbad Panigrahi

Reputation: 815

You can create a really simple 3-line function for this

const dogs = ["Poodle", "Beagle", "Bulldog"];
const cats = ["Bengal", "Siamese"];

const getAnimalType = (animal) => {
  if(dogs.includes(animal)) return `${animal} is a dog`
  if(cats.includes(animal)) return `${animal} is a cat`
  return `${animal} is not a dog or cat.`
}

const result = getAnimalType("Poodle");
console.log("result:" + result)

Upvotes: 1

Barmar
Barmar

Reputation: 781726

You can use an object to map animal types to functions.

function dog(animalType) {
  return animalType + " is a dog.";
}

function cat(animalType) {
  return animalType + " is a cat.";
}

function other(animalType) {
  return animalType + " is not a dog or cat.";
}

const typeMap = {
  Poodle: dog,
  Beagle: dog,
  Bulldog: dog,
  Bengal: cat,
  Siamese: cat
};

function getAnimalType(animalType) {
  let typeFun = typeMap[animalType] || other;
  return typeFun(animalType);
}

console.log(getAnimalType("Poodle"));

Upvotes: 3

CertainPerformance
CertainPerformance

Reputation: 370989

One option is an object indexed by dog or cat, whose values are arrays of animal types. This is easily extensible to additional animal types.

const animalNamesByType = {
  dog: ["Poodle", "Beagle", "Bulldog"],
  cat: ["Bengal", "Siamese"]
};
function getAnimalType(animal) {
  const entry = Object.entries(animalNamesByType).find(
    entry => entry[1].includes(animal)
  );
  return entry
  ? `${animal} is a ${entry[0]}`
  : `${animal} is not in animalNamesByType`;
}
console.log(getAnimalType("Poodle"));

Upvotes: 1

Related Questions