Git Product home page Git Product logo

adt's Introduction

adt CircleCI

adt is a library providing algebraic data types in Python, with a clean, intuitive syntax, and support for typing through a mypy plugin.

NOTE: This project is experimental, and not actively maintained by the author. Contributions and forking are more than welcome.

Table of contents:

  1. What are algebraic data types?
    1. Pattern matching
    2. Compared to Enums
    3. Compared to inheritance
    4. Examples in other programming languages
  2. Installation
    1. mypy plugin
  3. Defining an ADT
    1. Generated functionality
    2. Custom methods

What are algebraic data types?

An algebraic data type (also known as an ADT) is a way to represent multiple variants of a single type, each of which can have some data associated with it. The idea is very similar to tagged unions and sum types, which in Python are represented as Enums.

ADTs are useful for a variety of data structures, including binary trees:

@adt
class Tree:
    EMPTY: Case
    LEAF: Case[int]
    NODE: Case["Tree", "Tree"]

Abstract syntax trees (like you might implement as part of a parser, compiler, or interpreter):

@adt
class Expression:
    LITERAL: Case[float]
    UNARY_MINUS: Case["Expression"]
    ADD: Case["Expression", "Expression"]
    MINUS: Case["Expression", "Expression"]
    MULTIPLY: Case["Expression", "Expression"]
    DIVIDE: Case["Expression", "Expression"]

Or more generic versions of a variant type, like an Either type that represents a type A or a type B, but not both:

L = TypeVar('L')
R = TypeVar('R')

@adt
class Either(Generic[L, R]):
    LEFT: Case[L]
    RIGHT: Case[R]

Pattern matching

Now, defining a type isn't that interesting by itself. A lot of the expressivity of ADTs arises when you pattern match over them (sometimes known as "destructuring").

For example, we could use the Either ADT from above to implement a sort of error handling:

# Defined in some other module, perhaps
def some_operation() -> Either[Exception, int]:
    return Either.RIGHT(22)  # Example of building a constructor

# Run some_operation, and handle the success or failure
default_value = 5
unpacked_result = some_operation().match(
    # In this case, we're going to ignore any exception we receive
    left=lambda ex: default_value,
    right=lambda result: result)

Aside: this is very similar to how error handling is implemented in languages like Haskell, because it avoids the unpredictable control flow of raising and catching exceptions, and ensures that callers need to make an explicit decision about what to do in an error case.

One can do the same thing with the Expression type above (just more cases to match):

def handle_expression(e: Expression):
    return e.match(
        literal=lambda n: ...,
        unary_minus=lambda expr: ...,
        add=lambda lhs, rhs: ...,
        minus=lambda lhs, rhs: ...,
        multiply=lambda lhs, rhs: ...,
        divide=lambda lhs, rhs: ...)

Compared to Enums

ADTs are somewhat similar to Enums from the Python standard library (in fact, the uppercase naming convention is purposely similar).

For example, an Enum version of Expression might look like:

from enum import Enum, auto
class EnumExpression(Enum):
    LITERAL = auto()
    UNARY_MINUS = auto()
    ADD = auto()
    MINUS = auto()
    MULTIPLY = auto()
    DIVIDE = auto()

However, this doesn't allow data to be associated with each of these enum values. A particular value of Expression will tell you about a kind of expression that exists, but the operands to the expressions still have to be stored elsewhere.

From this perspective, ADTs are like Enums that can optionally have data associated with each case.

Compared to inheritance

Algebraic data types are a relatively recent introduction to object-oriented programming languages, for the simple reason that inheritance can replicate the same behavior.

Continuing our examples with the Expression ADT, here's how one might represent it with inheritance in Python:

from abc import ABC
class ABCExpression(ABC):
    pass

class LiteralExpression(ABCExpression):
    def __init__(self, value: float):
        pass

class UnaryMinusExpression(ABCExpression):
    def __init__(self, inner: ABCExpression):
        pass

class AddExpression(ABCExpression):
    def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
        pass

class MinusExpression(ABCExpression):
    def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
        pass

class MultiplyExpression(ABCExpression):
    def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
        pass

class DivideExpression(ABCExpression):
    def __init__(self, lhs: ABCExpression, rhs: ABCExpression):
        pass

This is noticeably more verbose, and the code to consume these types gets much more complex as well:

e: ABCExpression = UnaryMinusExpression(LiteralExpression(3))  # Example of creating an expression

if isinstance(e, LiteralExpression):
    result = ... # do something with e.value
elif isinstance(e, UnaryMinusExpression):
    result = ... # do something with e.inner
elif isinstance(e, AddExpression):
    result = ... # do something with e.lhs and e.rhs
elif isinstance(e, MinusExpression):
    result = ... # do something with e.lhs and e.rhs
elif isinstance(e, MultiplyExpression):
    result = ... # do something with e.lhs and e.rhs
elif isinstance(e, DivideExpression):
    result = ... # do something with e.lhs and e.rhs
else:
    raise ValueError(f'Unexpected type of expression: {e}')

ADTs offer a simple way to define a type which is one of a set of possible cases, and allowing data to be associated with each case and packed/unpacked along with it.

Examples in other programming languages

Algebraic data types are very common in functional programming languages, like Haskell or Scala, but they're gaining increasing acceptance in "mainstream" programming languages as well.

Here are a few examples.

Rust enums are actually full-fledged ADTs. Here's how an Either ADT could be defined:

enum Either<L, R> {
    Left(L),
    Right(R),
}

Swift enumerations are very similar to Rust's, and behave like algebraic data types through their support of "associated values."

enum Either<L, R> {
    case Left(L)
    case Right(R)
}

TypeScript offers ADTs through a language feature known as "discriminated unions".

See this example from their documentation:

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

Installation

To add adt as a library in your Python project, simply run pip (or pip3, as it may be named on your system):

pip install algebraic-data-types

This will install the latest version from PyPI.

mypy plugin

The library also comes with a plugin for mypy that enables typechecking of @adt definitions. If you are already using mypy, the plugin is required to avoid nonsensical type errors.

To enable the adt typechecking plugin, add the following to a mypy.ini file in your project's working directory:

[mypy]
plugins = adt.mypy_plugin

Defining an ADT

To begin defining your own data type, import the @adt decorator and Case[…] annotation:

from adt import adt, Case

Then, define a new Python class, upon which you apply the @adt decorator:

@adt
class MyADT1:
    pass

For each case (variant) that your ADT will have, declare a field with the Case annotation. It's conventional to declare your fields with ALL_UPPERCASE names, but the only true restriction is that they cannot be lowercase.

@adt
class MyADT2:
    FIRST_CASE: Case
    SECOND_CASE: Case

If you want to associate some data with a particular case, list the type of that data in brackets after Case (similar to the Generic[…] and Tuple[…] annotations from typing). For example, to add a case with an associated string:

@adt
class MyADT3:
    FIRST_CASE: Case
    SECOND_CASE: Case
    STRING_CASE: Case[str]

You can build cases with arbitrarily many associated pieces of data, as long as all the types are listed:

@adt
class MyADT4:
    FIRST_CASE: Case
    SECOND_CASE: Case
    STRING_CASE: Case[str]
    LOTS_OF_DATA_CASE: Case[int, str, str, Dict[int, int]]

ADTs can also be recursive—i.e., an ADT can itself be stored alongside a specific case—though the class name has to be provided in double quotes (a restriction which also applies to typing).

A typical example of a recursive ADT is a linked list. Here, the list is also made generic over a type T:

T = TypeVar('T')

@adt
class LinkedList(Generic[T]):
    NIL: Case
    CONS: Case[T, "LinkedList[T]"]

See the library's tests for more examples of complete ADT definitions.

Generated functionality

Given an ADT defined as follows:

@adt
class MyADT5:
    EMPTY: Case
    INTEGER: Case[int]
    STRING_PAIR: Case[str, str]

The @adt decorator will automatically generate accessor methods of the following form:

    def empty(self) -> None:
        return None

    def integer(self) -> int:
        ... # unpacks int value and returns it

    def string_pair(self) -> Tuple[str, str]:
        ... # unpacks strings and returns them in a tuple

These accessors can be used to obtain the data associated with the ADT case, but accessors will throw an exception if the ADT was not constructed with the matching case. This is a shorthand when you already know the case of an ADT object.

@adt will also automatically generate a pattern-matching method, which can be used when you don't know which case you have ahead of time:

    Result = TypeVar('Result')
    
    def match(self,
              empty: Callable[[], Result],
              integer: Callable[[int], Result],
              string_pair: Callable[[str, str], Result]) -> Result:
        if ... self was constructed as EMPTY ...:
            return empty()
        elif ... self was constructed as INTEGER ...:
            return integer(self.integer())
        elif ... self was constructed as STRING_PAIR ...:
            return string_pair(*self.string_pair())
        
        # if pattern match is incomplete, an exception is raised

See the library's tests for examples of using these generated methods.

@adt will also generate __repr__, __str__, and __eq__ methods (only if they are not defined already), to make ADTs convenient to use by default.

Custom methods

Arbitrary methods can be defined on ADTs by simply including them in the class definition as normal.

For example, to build "safe" versions of the default accessors on ExampleADT, which return None instead of throwing an exception when the case is incorrect:

@adt
class ExampleADT:
    EMPTY: Case
    INTEGER: Case[int]
    STRING_PAIR: Case[str, str]

    @property
    def safe_integer(self) -> Optional[int]:
        return self.match(empty=lambda: None,
                          integer=lambda n: n,
                          string_pair=lambda _a, _b: None)

    @property
    def safe_string_pair(self) -> Optional[Tuple[str, str]]:
        return self.match(empty=lambda: None,
                          integer=lambda n: None,
                          string_pair=lambda a, b: (a, b))

However, additional fields must not be added to the class, as the decorator will attempt to interpret them as ADT Cases (which will fail).

adt's People

Contributors

jspahrsummers avatar seanjennings1 avatar sebastienlavoie avatar wchresta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

adt's Issues

mypy plugin: match-function returning None does not type-check

Running the plugin with mypy==0.711 on the following code leads to an error:

from adt import adt, Case

@adt
class Expression:
    LITERAL: Case[float]

result: None = Expression.LITERAL(0.1).match(literal=lambda n: None)
error: Cannot infer type argument 1 of "match" of "Expression"

This does not happen when using something other than None as return values:

result: int = Expression.LITERAL(0.1).match(literal=lambda n: 1)

default option for pattern match

First, let just thank you so much for your work on this. Python is inescapable for many people working in data/machine learning, but any modern language should have sum types and pattern matching! Somehow Javascript sorted it out with Typescript. This really should be a standard feature with support for standard | sum type syntax 😖

Ok. Rant complete.

Are you considering adding a default option for pattern matching? I know that there are arguments against it, but otherwise you end up writing things like:

e.match(
  VAR1=lambda x: f(x)
  VAR2=lambda _: g()
  VAR3=lambda _: g()
  VAR4=lambda _: g()
  # you get the idea...
)

mypy plugin causes TypeError: list object expected; got tuple w/ mypy==0.812

It used to work with mypy==0.761 but broke when when upgraded to 0.812

Traceback (most recent call last):
  File "/usr/local/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/local/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "mypy/semanal.py", line 4835, in accept
  File "mypy/nodes.py", line 950, in accept
  File "mypy/semanal.py", line 1048, in visit_class_def
  File "mypy/semanal.py", line 1125, in analyze_class
  File "mypy/semanal.py", line 1134, in analyze_class_body_common
  File "mypy/semanal.py", line 1180, in apply_class_plugin_hooks
  File "/usr/local/lib/python3.8/site-packages/adt/mypy_plugin.py", line 187, in _transform_class
    _add_accessor_for_case(context, case)
  File "/usr/local/lib/python3.8/site-packages/adt/mypy_plugin.py", line 269, in _add_accessor_for_case
    return_type=case.accessor_return())
  File "/usr/local/lib/python3.8/site-packages/adt/mypy_plugin.py", line 145, in accessor_return
    return mypy.types.TupleType(
  File "mypy/types.py", line 1373, in __init__
TypeError: list object expected; got tuple[mypy.types.Instance, mypy.types.Instance]
/builds/ad53de79/0/path/to/my/file.py:30: error: INTERNAL ERROR -- Please try using mypy master on Github:
https://mypy.rtfd.io/en/latest/common_issues.html#using-a-development-mypy-build
Please report a bug at https://github.com/python/mypy/issues
version: 0.812

Using python 3.8.5

Collaboration about pattern matching and adts

Hi, Justin.

After several months, my project providing the very high performance pattern matching finally got released.

I wonder if we could mutually refer the projects in each README, then people can have a good instruction about how to work with both pattern matching and adt in Python.

Besides, my project does not work with static checker like Mypy due to my limited knowledge about writing mypy plugins. I'd ask if you could help me out of this, any stuff would be appreciated.

PyCharm can't recognizes that `Case` is callable and etc.

My Code:

@adt
class Result(Generic[S, E]):
    Success: Case[S]
    Error: Case[E]

    async def map(self, func: Callable[[S], Awaitable[Union[S, E]]]) -> "Result[S, E]":
        return self.match(success=lambda s: self.Success(func(s)), error=lambda e: e)

    async def flat_map(self, func: Callable[[S], Awaitable["Result[S, E]"]]) -> "Result[S, E]":
        return self.match(success=lambda s: (await func(s)), error=lambda e: e)

Pycharm said "Case object is not callable" for this line.

self.Success(func(s))

also intellisense doesn't work for completing self.match

I understand that this is architectural issue. to make pycharm intellisense work, I think we have to use base class, not use decorator.

this is just a suggestion. your lib works perfectly on runtime :)

Implement hash function for ADTs

According to the Python documentation:

The only required property is that objects which compare equal have the same hash value

At the moment, this isn't the case:

from adt import adt, Case


a1 = "abc"

a2 = "ab"
a2 += "c"

assert a1 == a2
assert hash(a1) == hash(a2)

@adt
class OptionStr:
    SOME: Case[str]
    NONE: Case

b1 = OptionStr.SOME(a1)
b2 = OptionStr.SOME(a2)

assert b1 == b2
assert hash(b1) == hash(b2)  # Fails

It looks like the fix is to add an additional function in adt/decorator.py – are there any subtleties that complicate that?

mypy plugin broken with mypy==0.730, works with 0.711

Hi! Nice work, thanks for publishing, absolutely appreciated.

Let's take a tiny example:

from adt import adt, Case

@adt
class Cmd:
    READKEY:   Case[str, str]
    WRITEKEY:  Case[str, str, str]
    DELETEKEY: Case[str, str]

This works just fine on the REPL, but the mypy plugin thing isn't giving it to me:

command.py:6: error: "Case" expects no type arguments, but 2 given
command.py:7: error: "Case" expects no type arguments, but 3 given
command.py:8: error: "Case" expects no type arguments, but 2 given
Found 3 errors in 1 file (checked 1 source file)

Of course I followed the README, and had put the lines in the project's setup.cfg:

[mypy]
plugins = adt.mypy_plugin

I'm abolutely sure that the plugin gets loaded: mypy spews errors as expected whenever I garble the config. But the complaints are still there. Python 3.6; mypy 0.730 — the freshest at pypi right now.

Do you see anything I'm missing? Or if you could perhaps retest with recent mypy, perhaps they broke the plugin...

mypy plugin: Example for safe_integer in README does not type-check.

When running mypy==0.711 against the following code taken from the README, mypy throws an error:

@adt
class ExampleADT:
    EMPTY: Case
    INTEGER: Case[int]
    STRING_PAIR: Case[str, str]

    @property
    def safe_integer(self) -> Optional[int]:
        return self.match(empty=lambda: None,
                          integer=lambda n: n,
                          string_pair=lambda a, b: None)
error: Incompatible return value type (got "None", expected "int")
error: Incompatible return value type (got "None", expected "int")

Function "bind" on Result adt doesn't typecheck

The following adt with an extra method bind is failing to typecheck. Mypy is returning

main.py:15: error: Argument "ok" to "match" of "Result" has incompatible type "Callable[[A], Result[C, B]]"; expected "Callable[[Result[C, B]], Result[C, B]]"

from adt import adt, Case
from typing import Generic, TypeVar, Callable

A = TypeVar("A")
B = TypeVar("B")
C = TypeVar("C")

@adt
class Result(Generic[A, B]):
    OK:  Case[A]
    ERR: Case[B]

    # bind :: Result a b -> (a -> Result c b) -> Result c b
    def bind(self, fun : Callable[[A], Result[C, B]]) -> Result[C, B]:
        return self.match(ok=fun, err=Result.ERR)

If I reveal the type of the match function, mypy says:

main.py:15: note: Revealed type is 'def [_MatchResult] (*, ok: def [_MatchResult] (A`1) -> _MatchResult`1, err: def [_MatchResult] (B`2) -> _MatchResult`1) -> _MatchResult`1'

and revealing the ok value:

main.py:15: note: Revealed type is 'def (A`1) -> main.Result[C`-1, B`2]'

So, the revealed types seems to be correct, but mypy still reports an error.

ADTs hashes are non unique

Lets say I have an ADT defined as:

@adt
class Foo:
        BAR = Case[str]
        BAZ = Case[str]

CAsting a list of adts as a set:

set([BAR("1"), BAR("1"), BAZ("1")])

will return:

set(BAR("1"), BAR("1"), BAZ("1"))

instead of:

set([BAR("1"), BAZ("1")])

avoid dangerous designs & suggestions

First, about #13,
FYI, Pampy is not a real pattern matching(hereafter PM) library. It is atmost an implememtation of an interpreter that supports very limited and inextensible PM. Further, there has already been thousands of "plausive patterm matching" implemented for Python, and IMHO MacroPy could be a more correct one. Pampy made use of the information asymmetry to be attractive(even the 10-year-ago similar libraries didn't boast that way), and finally successfully become a cheater.

Your notations is a bit similar to Pampy, and a.match(case=action_under_case) seems to be so much evil, unmaintainable.

More,
I suggest you to focus on ADTs instead of getting burdened from the mess of "pattern matching for PY". We can use method dispatch to achieve a part of PM's functionality, as you know we have typing.overload which works well.

Besides, there could be some feasible ways to achieve the true PM for Python without requiring any new syntax constructs:

Core idea: convert code objects(sorry you cannot get ASTs in runtime, except you choose the evil and unreliable inspect.getsource), to change semantics for specific structures; simultaneously add type checking support. E.g., you can use the notation like

with val_to_match:
    if Case(p1, p2, ...):
        ...
    if ....

to denote

case val_to_match of
    Case(p1, p2, ...) -> ...
     ...

HOW TO

  • use the library uncompyler to convert code objects into ASTs/analyze code objects to recognize the semantics.
  • transform ASTs/your own semantics representations, and rewrite specific structures to have PM semantics.
  • compile, get your new code objects and replace your original functions with them.

You might also google CASE TREE or TREE PATTERN MATCHING for the canonical algorithms of PM.

sealed classes

There is some discussion on typing-sig about sealed classes here:
https://mail.python.org/archives/list/[email protected]/thread/QSCT2N4RFPRQN2U7NIX6VCNVNDHGO22U/

Perhaps you can chime in with your thoughts as the author of this library.

@sealed
class Tree:
    EMPTY: None
    LEAF: Leaf
    NODE: Node
  
@dataclass  
class Leaf:
   data: int
   
@dataclass
class Node:
   left: Tree
   right: Tree  

would be nice. Other python libraries with similar functionality linked from here:

https://www.reddit.com/r/Python/comments/ipqqca/sum_types_discriminated_unions_for_python/

is pretty nice syntactically.

Examine integration/overlap with pampy

Found out about pampy after starting this project. It seems like it will provide a lot of similar utility vis-a-vis pattern matching, though it doesn't particularly make the construction/definition of algebraic data types easier.

My instinct is that we should integrate with it really nicely (which might also clean up our own pattern matching abstraction), but otherwise this library still offers additional value—but open to other feedback here.

Named fields

@adt
class ExampleADT:
    EMPTY: Case
    INTEGER: Case[int]
    STRING_PAIR: Case[str, str]

First, I love what you are doing with this library. There is just one thing holding me back from using it. I would like to name the fields, rather than access them with positional indexing. Something like what I am showing below.

@adt
class ExampleADT:
    EMPTY: Case
    INTEGER: Case[int]
    STRING_PAIR: Case[foo: str, bar: str]

The accessor method could return a NamedTuple instead of just a Tuple.

def string_pair(self) -> Tuple[str, str]:
        # unpacks strings and returns them in a tuple

Add support for matching using context managers.

One issue I have with the match function is that it relies on either using lambdas (which can only contain one line) or if you need multiple lines, first defining the handler function and then passing it to the match function.

An option that I think is worth considering is using context managers for pattern matching. The main drawback is that it would not allow returning a value like with the match function, but the syntax is nicer when the case handlers contain more than one statement. As a proof of concept I have implemented this new interface while keeping the library backwards compatible.

Here is some example code of the new proposed syntax.

@adt
class ContextMatching:
    EMPTY: Case
    INTEGER: Case[int]
    STRINGS: Case[str, str]

foo = ContextMatching.INTEGER(1)

with foo.empty:
    print("Is empty")

with foo.integer as value:
    print("Is integer:", value)

with foo.strings as (string_1, string_2):
    print("Is strings:", string_1, string_2)

This example will end up printing out Is integer: 1

This opens up the possibility for matching values as well. Although not implemented yet I believe code such as this is possible to implement while keeping the current API intact.

@adt
class ContextMatching:
    NONE: Case
    OK: Case[int, float]

with foo:
    with foo.ok[:4, :] as (val_1, val_2):
        print("val_1 less than 4 and val_2 anything")
    
    with foo.ok[4:, 1.3:9.9] as (val_1, val_2):
        print("val_1 4 or higher and val_2 between 1.3 and 9.9")
    
    with foo.ok as (val_1, val_2):
        print("Unhandled ok cases")

Mixed-case case names not accepted by .match(), but README says they're fine

The README says:

It's conventional to declare your fields with ALL_UPPERCASE names, but the only true restriction is that they cannot be lowercase.

If I understand this correctly, it means that the only kind of names that are invalid are names that are fully in lowercase. That is, FOO_BAR, FooBar, and foo_Bar are valid names, but foobar and foo_bar aren't.

However, match method does not seem to work nicely with names that are not fully in uppercase. The following code is supposed to work:

from adt import adt, Case

@adt
class Foo:
    Bar: Case
    Baz: Case

value = Foo.Bar()
print(value.match(
    bar=lambda: 'bar',
    baz=lambda: 'baz',
))

But it fails with the error: ValueError: Unrecognized case BAR in pattern match against <<class '__main__.Foo'>.Bar: None> (expected one of dict_keys(['Bar', 'Baz'])).

The code of match seems to convert the names of variants to upper case, which may not be correct when original names contain lowercase letters (provided that such names are allowed).

I see two possible solutions here.

  1. Forbidding variant names not in the format ALL_UPPERCASE, stating this in the README and perhaps enforcing this rule in the decorator in order to prevent accidental mistakes. This may be backwards-incompatible, but is probably the easiest solution. As a compromise, it is possible to deprecate names containing lowercase characters and possibly to warn about them in the decorator.
  2. Modifying the code to accept non-fully uppercase names. This is tricky because we either have to continue accepting case-insensitive variant names in match and somehow convert them to their original case, or accept only names in their original case (that is, one would only be able to write something like value.match(Bar=..., Baz=...), not value.match(bar=..., baz=...)). The former may be tricky to implement, and the latter would probably totally break backward compatibility.

Out of these two, I personally prefer the first solution, but it's just my opinion on this problem.

P.S. I used the term variant as a synonym of case here to avoid confusion between lower/upper case and ADT cases.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.