diff --git a/DEMO.ipynb b/DEMO.ipynb new file mode 100644 index 0000000..afdd81d --- /dev/null +++ b/DEMO.ipynb @@ -0,0 +1,710 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Pampy in Star Wars](https://raw.githubusercontent.com/santinic/pampy/master/imgs/pampy.png \"Pampy in Star Wars\")\n", + "\n", + "# Pampy: Pattern Matching for Python\n", + "[![License MIT](https://go-shields.herokuapp.com/license-MIT-blue.png)]()\n", + "[![Travis-CI Status](https://api.travis-ci.org/santinic/pampy.svg?branch=master)](https://travis-ci.org/santinic/pampy)\n", + "[![Coverage Status](https://coveralls.io/repos/github/santinic/pampy/badge.svg?branch=master)](https://coveralls.io/github/santinic/pampy?branch=master)\n", + "[![PyPI version](https://badge.fury.io/py/pampy.svg)](https://badge.fury.io/py/pampy)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Pampy is pretty small (150 lines), reasonably fast, and often makes your code more readable\n", + "and hence easier to reason about.\n", + "\n", + "\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can write many patterns\n", + "\n", + "Patterns are evaluated in the order they appear.\n", + "\n", + "\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can write Fibonacci\n", + "\n", + "The operator _ means \"any other case I didn't think of\"." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from pampy import match, _\n", + "\n", + "def fibonacci(n):\n", + " return match(n,\n", + " 1, 1,\n", + " 2, 1,\n", + " _, lambda x: fibonacci(x-1) + fibonacci(x-2)\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "55" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fibonacci(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can write a Lisp calculator in 5 lines" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from functools import reduce\n", + "from pampy import match, REST, _\n", + "\n", + "def lisp(exp):\n", + " return match(exp,\n", + " int, lambda x: x,\n", + " callable, lambda x: x,\n", + " (callable, REST), lambda f, rest: f(*map(lisp, rest)),\n", + " tuple, lambda t: list(map(lisp, t)),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "plus = lambda a, b: a + b\n", + "minus = lambda a, b: a - b" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lisp((plus, 1, 2))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lisp((plus, 1, (minus, 4, 2)))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lisp((reduce, plus, (range, 10)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can match so many things!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "match(x,\n", + " 3, \"this matches the number 3\",\n", + "\n", + " int, \"matches any integer\",\n", + "\n", + " (str, int), lambda a, b: \"a tuple (a, b) you can use in a function\",\n", + "\n", + " [1, 2, _], \"any list of 3 elements that begins with [1, 2]\",\n", + "\n", + " {'x': _}, \"any dict with a key 'x' and any value associated\",\n", + "\n", + " _, \"anything else\"\n", + ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can match [HEAD, TAIL]" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "from pampy import match, HEAD, TAIL, _\n", + "\n", + "x = [1, 2, 3]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[2, 3]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "match(x, [1, TAIL], lambda t: t)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(1, [2, 3])" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "match(x, [HEAD, TAIL], lambda h, t: (h, t))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`TAIL` and `REST` actually mean the same thing." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can nest lists and tuples" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, [2, 3], 4]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pampy import match, _\n", + "\n", + "x = [1, [2, 3], 4]\n", + "\n", + "match(x, [1, [_, 3], _], lambda a, b: [1, [a, 3], b])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can nest dicts. And you can use _ as key!" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pet = { 'type': 'dog', 'details': { 'age': 3 } }\n", + "\n", + "match(pet, { 'details': { 'age': _ } }, lambda age: age)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('details', 3)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "match(pet, { _ : { 'age': _ } }, lambda a, b: (a, b))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It feels like putting multiple _ inside dicts shouldn't work. Isn't ordering in dicts not guaranteed ?\n", + "But it does because\n", + "[in Python 3.7, dict maintains insertion key order by default](https://mail.python.org/pipermail/python-dev/2017-December/151283.html)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## You can match class hierarchies" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "class Pet: pass\n", + "class Dog(Pet): pass\n", + "class Cat(Pet): pass\n", + "class Hamster(Pet): pass\n", + "\n", + "def what_is(x):\n", + " return match(x,\n", + " Dog, \t\t'dog',\n", + " Cat, \t\t'cat',\n", + " Pet, \t\t'any other pet',\n", + " _, \t\t'this is not a pet at all',\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'cat'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is(Cat())" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'dog'" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is(Dog())" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'any other pet'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is(Hamster())" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'any other pet'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is(Pet())" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'this is not a pet at all'" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## All the things you can match\n", + "\n", + "As Pattern you can use any Python type, any class, or any Python value.\n", + "\n", + "The operator `_` and built-in types like `int` or `str`, extract variables that are passed to functions.\n", + "\n", + "Types and Classes are matched via `instanceof(value, pattern)`.\n", + "\n", + "`Iterable` Patterns match recursively through all their elements. The same goes for dictionaries.\n", + "\n", + "| Pattern Example | What it means | Matched Example | Arguments Passed to function | NOT Matched Example |\n", + "| --------------- | --------------| --------------- | ----------------------------- | ------------------ |\n", + "| `\"hello\"` | only the string `\"hello\"` matches | `\"hello\"` | nothing | any other value |\n", + "| `None` | only `None` | `None` | nothing | any other value |\n", + "| `int` | Any integer | `42` | `42` | any other value |\n", + "| `float` | Any float number | `2.35` | `2.35` | any other value |\n", + "| `str` | Any string | `\"hello\"` | `\"hello\"` | any other value |\n", + "| `tuple` | Any tuple | `(1, 2)` | `(1, 2)` | any other value |\n", + "| `list` | Any list | `[1, 2]` | `[1, 2]` | any other value |\n", + "| `MyClass` | Any instance of MyClass. **And any object that extends MyClass.** | `MyClass()` | that instance | any other object |\n", + "| `_` | Any object (even None) | | that value | |\n", + "| `ANY` | The same as `_` | | that value | |\n", + "| `(int, int)` | A tuple made of any two integers | `(1, 2)` | `1` and `2` | (True, False) |\n", + "| `[1, 2, _]` | A list that starts with 1, 2 and ends with any value | `[1, 2, 3]` | `3` | `[1, 2, 3, 4]` |\n", + "| `[1, 2, TAIL]` | A list that start with 1, 2 and ends with any sequence | `[1, 2, 3, 4]`| `[3, 4]` | `[1, 7, 7, 7]` |\n", + "| `{'type':'dog', age: _ }` | Any dict with `type: \"dog\"` and with an age | `{\"type\":\"dog\", \"age\": 3}` | `3` | `{\"type\":\"cat\", \"age\":2}` |\n", + "| `{'type':'dog', age: int }` | Any dict with `type: \"dog\"` and with an `int` age | `{\"type\":\"dog\", \"age\": 3}` | `3` | `{\"type\":\"dog\", \"age\":2.3}` |\n", + "| `re.compile('(\\w+)-(\\w+)-cat$')` | Any string that matches that regular expression expr | `\"my-fuffy-cat\"` | `\"my\"` and `\"puffy\"` | `\"fuffy-dog\"` | " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using strict=False\n", + "\n", + "By default `match()` is strict. If no pattern matches, it raises a `MatchError`.\n", + "\n", + "You can prevent it using `strict=False`. In this case `match` just returns `False` if nothing matches." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "ename": "MatchError", + "evalue": "'_' not provided. This case is not handled:\n[1, 2]", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mMatchError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mmatch\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"whatever\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m/usr/local/lib/python3.6/site-packages/pampy/pampy.py\u001b[0m in \u001b[0;36mmatch\u001b[0;34m(var, strict, *args)\u001b[0m\n\u001b[1;32m 155\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mstrict\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 156\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0m_\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mpatterns\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 157\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mMatchError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"'_' not provided. This case is not handled:\\n%s\"\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mvar\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 158\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 159\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mMatchError\u001b[0m: '_' not provided. This case is not handled:\n[1, 2]" + ] + } + ], + "source": [ + "match([1, 2], [1, 2, 3], \"whatever\")" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "match([1, 2], [1, 2, 3], \"whatever\", strict=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using Regular Expressions\n", + "Pampy supports Python's Regex. You can pass a compiled regex as pattern, and Pampy is going to run `patter.search()`, and then pass to the action function the result of `.groups()`." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "attributes": { + "classes": [ + "python " + ], + "id": "" + } + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "def what_is(pet):\n", + " return match(pet,\n", + " re.compile('(\\w+)-(\\w+)-cat$'), lambda name, my: 'cat '+name,\n", + " re.compile('(\\w+)-(\\w+)-dog$'), lambda name, my: 'dog '+name,\n", + " _, \"something else\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'dog fuffy'" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is('fuffy-my-dog')" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'dog puffy'" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is('puffy-her-dog')" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'cat carla'" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is('carla-your-cat')" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'something else'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what_is('roger-my-hamster')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README.md b/README.md index 13970e9..d71acd1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ and hence easier to reason about. [There is also a JavaScript version, called Pa +## Demo + +Try `pampy` interactively in this online demo: + +[![](https://cdn-images-1.medium.com/max/1600/1*cI91DR6og9iF06hBrHKINg.png)](https://notebooks.ai/demo/gh/santinic/pampy) + ## You can write many patterns Patterns are evaluated in the order they appear. diff --git a/demo.yml b/demo.yml new file mode 100644 index 0000000..b2c6e6f --- /dev/null +++ b/demo.yml @@ -0,0 +1,2 @@ +requirements: + - pampy==0.1.9 \ No newline at end of file