admin管理员组文章数量:1289558
Consider the following code snippet:
if (msg.operation == 'create') {
model.blocks.push(msg.block)
drawBlock(msg.block);
} else if (msg.operation == 'select' && msg.properties.snap == 'arbitrary') {
doStuff(msg.properties.x, msg.properties.y);
} else if (msg.operation == 'unselect') {
doOtherStuff(msg.properties.geometry);
}
Is there a way to refactor this so I can pattern match on msg
, akin to the following invalid code:
msg match {
case { operation: 'create', block: b } =>
model.blocks.push(b);
drawBlock(b);
case { operation: 'select', properties: { snap: 'arbitrary', x: sx, y: sy } } =>
doStuff(sx, sy);
case { operation: 'unselect', properties: { snap: 'specific' }, geometry: geom } =>
doOtherStuff(geom);
}
Alternatively, what would be the most idiomatic way of achieving this in ES6, without the ugly if-then-else
chain?
Update. Granted that this is a simplistic example where a full-blown pattern matching is probably unneeded. But one can imagine a scenario of matching arbitrary hierarchical pieces of a long AST.
TL;DR; the power of destructuring, acpanied with an automatic check if it is possible to do it or not.
Consider the following code snippet:
if (msg.operation == 'create') {
model.blocks.push(msg.block)
drawBlock(msg.block);
} else if (msg.operation == 'select' && msg.properties.snap == 'arbitrary') {
doStuff(msg.properties.x, msg.properties.y);
} else if (msg.operation == 'unselect') {
doOtherStuff(msg.properties.geometry);
}
Is there a way to refactor this so I can pattern match on msg
, akin to the following invalid code:
msg match {
case { operation: 'create', block: b } =>
model.blocks.push(b);
drawBlock(b);
case { operation: 'select', properties: { snap: 'arbitrary', x: sx, y: sy } } =>
doStuff(sx, sy);
case { operation: 'unselect', properties: { snap: 'specific' }, geometry: geom } =>
doOtherStuff(geom);
}
Alternatively, what would be the most idiomatic way of achieving this in ES6, without the ugly if-then-else
chain?
Update. Granted that this is a simplistic example where a full-blown pattern matching is probably unneeded. But one can imagine a scenario of matching arbitrary hierarchical pieces of a long AST.
TL;DR; the power of destructuring, acpanied with an automatic check if it is possible to do it or not.
Share Improve this question edited Jun 14, 2022 at 9:11 icc97 12.9k9 gold badges83 silver badges97 bronze badges asked Mar 28, 2017 at 0:25 Hugo Sereno FerreiraHugo Sereno Ferreira 8,6317 gold badges50 silver badges92 bronze badges 7- 4 Nope! Nothing like that is built into JavaScript. – Ry- ♦ Commented Mar 28, 2017 at 0:26
- Probably wouldn't be that hard to write a function that accepts a value and a var-arg list of objects and have it check each. – Carcigenicate Commented Mar 28, 2017 at 0:28
-
2
why can't you
switch(msg.operation) { case 'create':
etc - I don't understand the point ofblock:b
andid:id
in your pseudo code – Jaromanda X Commented Mar 28, 2017 at 0:32 - 1 "most idiomatic" is a new phrase for getting around the restriction for opinion-based questions. – Heretic Monkey Commented Mar 28, 2017 at 0:35
-
1
But one can simply imagine
- not from your original question - even now, one can not imagine what you want - perhaps you should be less vague? – Jaromanda X Commented Mar 28, 2017 at 0:41
5 Answers
Reset to default 5You could write a match
function like this, which (when bined with arrow functions and object destructuring) is fairly similar to the syntax your example:
/**
* Called as:
* match(object,
* pattern1, callback1,
* pattern2, callback2,
* ...
* );
**/
function match(object, ...args) {
for(let i = 0; i + 1 < args.length; i += 2) {
const pattern = args[i];
const callback = args[i+1];
// this line only works when pattern and object both are JS objects
// you may want to replace it with a more prehensive check for
// all types (objects, arrays, strings, null/undefined etc.)
const isEqual = Object.keys(pattern)
.every((key) => object[key] === pattern[key]);
if(isEqual)
return callback(object);
}
}
// -------- //
const msg = { operation: 'create', block: 17 };
match(msg,
{ operation: 'create' }, ({ block: b }) => {
console.log('create', b);
},
{ operation: 'select-block' }, ({ id: id }) => {
console.log('select-block', id);
},
{ operation: 'unselect-block' }, ({ id: id }) => {
console.log('unselect-block', id);
}
);
I think @gunn's answer is onto something good here, but the primary issue I have with his code is that it relies upon a side-effecting function in order to produce a result – his match
function does not have a useful return value.
For the sake of keeping things pure, I will implement match
in a way that returns a value. In addition, I will also force you to include an else
branch, just the way the ternary operator (?:
) does - matching without an else
is reckless and should be avoided.
Caveat: this does not work for matching on nested data structures but support could be added
// match.js
// only export the match function
const matchKeys = x => y =>
Object.keys(x).every(k => x[k] === y[k])
const matchResult = x => ({
case: () => matchResult(x),
else: () => x
})
const match = x => ({
case: (pattern, f) =>
matchKeys (pattern) (x) ? matchResult(f(x)) : match(x),
else: f => f(x)
})
// demonstration
const myfunc = msg => match(msg)
.case({operation: 'create'}, ({block}) => ['create', block])
.case({operation: 'select-block'}, ({id}) => ['select-block', id])
.case({operation: 'unselect-block'}, ({id}) => ['unselect-block', id])
.else( (msg) => ['unmatched-operation', msg])
const messages = [
{operation: 'create', block: 1, id: 2},
{operation: 'select-block', block: 1, id: 2},
{operation: 'unselect-block', block: 1, id: 2},
{operation: 'other', block: 1, id: 2}
]
for (let m of messages)
// myfunc returns an actual value now
console.log(myfunc(m))
// [ 'create', 1 ]
// [ 'select-block', 2 ]
// [ 'unselect-block', 2 ]
// [ 'unmatched-operation', { operation: 'other', block: 1, id: 2 } ]
not quite pattern matching
Now actual pattern matching would allow us to destructure and match in the same expression – due to limitations of JavaScript, we have to match in one expression and destructure in another. Of course this only works on natives that can be destructured like {}
and []
– if a custom data type was used, we'd have to dramatically rework this function and a lot of conveniences would be lost.
You can use a higher order function and destructuring assignment to get something remotely similar to pattern matching:
const _switch = f => x => f(x);
const operationSwitch = _switch(({operation, properties: {snap, x, y, geometry}}) => {
switch (operation) {
case "create": {
let x = true;
return operation;
}
case "select": {
let x = true;
if (snap === "arbitrary") {
return operation + " " + snap;
}
break;
}
case "unselect": {
let x = true;
return operation;
}
}
});
const msg = {operation: "select", properties: {snap: "arbitrary", x: 1, y: 2, geometry: "foo"}};
console.log(
operationSwitch(msg) // select arbitrary
);
By putting the switch
statement in a function we transformed it to a lazy evaluated and reusable switch
expression.
_switch
es from functional programming and is usually called apply
or A
. Please note that I wrapped each case
into brackets, so that each code branch has its own scope along with its own optional let
/const
bindings.
If you want to pass _switch
more than one argument, just use const _switchn = f => (...args) => f(args)
and adapt the destructuring to [{operation, properties: {snap, x, y, geometry}}]
.
However, without pattern matching as part of the language you lose many of the nice features:
- if you change the type of
msg
, there are no automatic checks and_switch
may silently stop working - there are no automatic checks if you covering all cases
- there are no checks on tag name typos
The decisive question is whether it is worth the effort to introduce a technique that is somehow alien to Javascript.
Sure, why not?
function match(object) {
this.case = (conditions, fn)=> {
const doesMatch = Object.keys(conditions)
.every(k=> conditions[k]==object[k])
if (doesMatch) fn(object)
return this
}
return this
}
// Example of use:
const msg = {operation: 'create', block: 5}
match(msg)
.case({ operation: 'create'}, ({block})=> console.log('create', block))
.case({ operation: 'select-block'}, ({id})=> console.log('select-block', id))
.case({ operation: 'unselect-block'}, ({id})=> console.log('unselect-block', id))
Given there's no easy way to properly do this until TC39 implemented switch
pattern matching es along, the best bet is libraries for now.
loadash
Go ol' loadash has a nice _.cond
function:
var func = _.cond([
[_.matches({ 'a': 1 }), _.constant('matches A')],
[_.conforms({ 'b': _.isNumber }), _.constant('matches B')],
[_.stubTrue, _.constant('no match')]
]);
func({ 'a': 1, 'b': 2 });
// => 'matches A'
func({ 'a': 0, 'b': 1 });
// => 'matches B'
func({ 'a': '1', 'b': '2' });
// => 'no match'
pat
One of the remended libraries to look at, which has feature parity with the TC39 proposal for switch
pattern matching, pat, is quite small and nicely written - this is the main index.js
:
import { oneOf } from './matchers/index.js'
export * from './matchers/index.js'
export * from './mappers.js'
export const match =
(value) =>
(...matchers) => {
const result = oneOf(...matchers)(value)
return result.value
}
Here's the simple example:
import {match, when, otherwise, defined} from 'pat'
function greet(person) {
return match (person) (
when (
{ role: 'student' },
() => 'Hello fellow student.'
),
when (
{ role: 'teacher', surname: defined },
({ surname }) => `Good morning ${surname} sensei.`
),
otherwise (
() => 'STRANGER DANGER'
)
)
}
So for yours something like this should work:
match (msg) (
when ({ operation: 'create' }), ({ block: b }) => {
model.blocks.push(b);
drawBlock(b);
}),
when ({ operation: 'select', properties: { snap: 'arbitrary' } }), ({ properties: { x: sx, y: sy }}) =>
doStuff(sx, sy)
)
when ({ operation: 'unselect', properties: { snap: 'specific' } }, ({ geometry: geom }) =>
doOtherStuff(geom)
)
)
match-iz
For people wanting to implement the whole thing themselves there is a remended small library match-iz that implements functional pattern matching in currently 194 lines.
Supercharged switch
I'm wondering if something like this 'supercharged switch' might get close to what your after:
const match = (msg) => {
const { operation, properties: { snap } } = msg;
switch (true) {
case operation === 'create':
model.blocks.push(b);
drawBlock(b);
break;
case operation === 'select' && snap === 'arbitrary':
const { properties: { x: sx, y: sy }} = msg;
doStuff(sx, sy);
break;
case operation === 'unselect' && snap === 'specific':
const { geometry: geom } = msg;
doOtherStuff(geom)
break;
}
}
Reducers
Also the whole concept of matching on strings within objects and then running a function based on that sounds a lot like Redux reducers.
From an earlier answer of mine about reducers:
const operationReducer = function(state, action) {
const { operation, ...rest } = action
switch (operation) {
case 'create':
const { block: b } = rest
return createFunc(state, b);
case 'select':
case 'unselect':
return snapReducer(state, rest);
default:
return state;
}
};
const snapReducer = function(state, action) {
const { properties: { snap } } = action
switch (snap) {
case 'arbitrary':
const { properties: { x: sx, y: sy } } = rest
return doStuff(state, sx, sy);
case 'specific':
const { geometry: geom } = rest
return doOtherStuff(state, geom);
default:
return state;
}
};
本文标签: javascriptES6 pattern match in SwitchStack Overflow
版权声明:本文标题:javascript - ES6 pattern match in Switch - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741402575a2376757.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论