Last active
August 29, 2015 13:57
-
-
Save pwightman/9679932 to your computer and use it in GitHub Desktop.
Pattern matching, taken from functional programming languages like Clojure/Racket, applied to Objective-C.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Example Goal: find URLs embedded in arbitrary JSON | |
NSDictionary *JSON = @{ | |
@"some": @[ | |
@"arbitrary": @[ | |
@"json", | |
@"http://google.com", | |
@{ @"url": @"http://yahoo.com" } | |
] | |
] | |
}; | |
// ZSPatternMatcher would (initially) match on JSON-compatible classes (`NSString`, `NSArray`, `NSDictionary`, etc...) | |
// should maybe indicate that in the class/method names | |
ZSPatternMatcher *matcher = [ZSPatternMatcher matcherWithSource:JSON]; | |
// Enumerates all strings in arrays, or strings as values in objects | |
[[[matcher valuesMatchingClass:NSString.class] | |
filter:^BOOL(id obj) { | |
return [urlRegex matches:obj]; // I'm making this up, roll with me | |
}] | |
each:^(id obj) { | |
// do stuff | |
}]; | |
// This | |
[matcher valuesMatchingClass:NSString.class] | |
// is really just a special case of something more like: | |
[matcher valuesMatching:^BOOL(id obj) { return obj.class == NSString.class }]; | |
// which is really just a special case of this, where allValues recursively returns | |
// all values (array elements or values in ditionaries) | |
[[matcher allValues] filter:^BOOL(id obj) { return obj.class == NSString.class }]; | |
// You could also do allKeys to recursively get all keys in all dictionaries | |
[matcher allKeys] | |
/* | |
While this imperative matching is nice in its own right, declarative matching based | |
on *structure* would be awesome/better. | |
*/ | |
// It could be used to find occurences of exact, static elements in JSON | |
[matcher JSONObjectsMatching:@{ @"url": @"http://google.com" }].count; | |
// It could also match on more vague, dynamic definitions, aptly called "patterns" | |
[matcher JSONObjectsMatching:@{ @"url": ZSPatternAnyString }]; | |
// which is just a special case of something like | |
[matcher JSONObjectsMatching:@{ | |
@"url": [ZSPattern patternWithTest:^BOOL(id obj) { return obj.class == NSString.class; }] | |
}]; | |
// You could have several macros defined for common things: | |
ZSPatternAnyObject | |
ZSPatternAnyString | |
ZSPatternAnyNumber | |
ZSPatternAnyDate | |
ZSPatternAnyArray | |
ZSPatternAnyDictionary | |
// Maybe class methods would be better: | |
[ZSPattern anyString]; | |
// Eh, it's just syntactic. Let's move on. | |
// You could also mix static data with patterns | |
[matcher JSONObjectsMatching:@{ @"nums": @[@1, @2, ZSPatternAnyNumber] }] | |
// ZSPattern's API would be something simple like: | |
[ZSPatternAnyString objectPasses:@1]; // => NO | |
[ZSPatternAnyString objectPasses:@"hi"]; // => YES | |
// Honestly, you could just use straight-up blocks and forget ZSPattern as a class, without losing | |
// the nice macros. | |
[matcher JSONObjectsMatching:@{ @"url": ^BOOL(id obj) { return YES; } }]; | |
// My only concern with this is that blocks are (I think?) rather opaque at runtime... not sure how | |
// easy it is to "detect" blocks, ensure they're the right return type/parameters, etc.. I've had | |
// very mysterious runtime bugs crop up in the past based on slight variations in block definitions | |
// when unpacking blocks from NSArray/NSDictionary objects. | |
// UPDATE: Opaque, indeed: http://stackoverflow.com/questions/9048305/checking-objective-c-block-type | |
// You could also make patterns composable. I'm building an interpreter where it would be useful to | |
// know when you've run across the "atoms" (irreducible parts of a language's grammar) of JSON | |
ZSPattern *atoms = [ZSPattern orPatternForPatterns:@[ | |
ZSPatternAnyString, | |
ZSPatternAnyBool, | |
ZSPatternAnyNumber | |
]]; | |
// Alternative syntax: | |
ZSPattern *atoms = [[ZSPatternAnyString or:ZSPatternAnyBool] or:ZSPatternAnyNumber]; | |
// Finally, you could also do something that's syntactically more along the lines of Clojure/Racket's | |
// `match`, (link: https://github.com/clojure/core.match#example-usage) | |
// Here's a FizzBuzz example: | |
for (NSInteger num = 0; num < 100; num++) { | |
[matcher match:@[@(num % 3), @(num % 5)] clauses:@[ | |
@[@0, @0]: ^(id obj) { | |
NSLog(@"FizzBuzz"); | |
}, | |
@[@0, ZSPatternAnyNumber]: ^(id obj) { | |
NSLog(@"Fizz"); | |
}, | |
@[ZPatternAnyNumber, @0]: ^(id obj) { | |
NSLog(@"Buzz"); | |
} | |
]; | |
} | |
// For doing transforms on matches, there could also be a replaceMatch:clauses | |
NSArray *transformed = [matcher replaceMatch:@[ @1, @2, @3 ] clauses:@{ | |
ZSPatternAnyNumber: ^id(id obj) { | |
return @([obj integerValue] + 1); | |
} | |
}]; // => @[ @2, @3, @4 ] | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment