from __future__ import (division as _py3_division,
print_function as _py3_print,
absolute_import as _py3_abs_import)
import functools
import types
import string
import sys
from collections import namedtuple
from xoutil.eight.meta import metaclass
from .hindley_milner import TypeVariable
from .hindley_milner import TypeOperator
from .hindley_milner import Var
from .hindley_milner import App
from .hindley_milner import Lam
from .hindley_milner import unify
from .hindley_milner import analyze
from .hindley_milner import Function
from .hindley_milner import Tuple
from .hindley_milner import ListType
if sys.version[0] == '2':
lowercase = string.lowercase
else:
lowercase = string.ascii_lowercase
if sys.version[0] == '2':
__python_builtins__ = set((
types.BooleanType, types.BufferType, types.BuiltinFunctionType,
types.BuiltinMethodType, types.ClassType, types.CodeType,
types.ComplexType, types.DictProxyType, types.DictType,
types.DictionaryType, types.EllipsisType, types.FileType,
types.FloatType, types.FrameType, types.FunctionType,
types.GeneratorType, types.GetSetDescriptorType, types.InstanceType,
types.IntType, types.LambdaType, types.ListType, types.LongType,
types.MemberDescriptorType, types.MethodType, types.ModuleType,
types.NoneType, types.NotImplementedType, types.ObjectType,
types.SliceType, types.StringType, types.StringTypes,
types.TracebackType, types.TupleType, types.TypeType,
types.UnboundMethodType, types.UnicodeType, types.XRangeType, set,
frozenset))
__python_function_types__ = set((
types.FunctionType, types.LambdaType, types.MethodType,
types.UnboundMethodType, types.BuiltinFunctionType,
types.BuiltinMethodType))
else:
__python_builtins__ = set((
bool, dict, type(Ellipsis), float, int, type(None), str, tuple,
complex, list, set, frozenset, slice,
type, types.BuiltinFunctionType, types.BuiltinMethodType,
types.CodeType, types.DynamicClassAttribute, types.FrameType,
types.FunctionType, types.GeneratorType, types.GetSetDescriptorType,
types.LambdaType, types.MappingProxyType, types.MemberDescriptorType,
types.MethodType, types.ModuleType, types.TracebackType))
__python_function_types__ = set((
types.FunctionType, types.LambdaType, types.MethodType,
types.BuiltinFunctionType, types.BuiltinMethodType))
def is_collection(m):
'''True if `m` is a collection.
strings are not collections.
'''
return hasattr(m, '__iter__') and not isinstance(m, str)
def is_builtin(cls):
"""
Test whether a class or type is a Python builtin.
Args:
cls: The class or type to examine
Returns:
True if a type is a Python builtin type, and False otherwise.
"""
return cls in __python_builtins__
def nt_to_tuple(nt):
"""
Convert an instance of namedtuple (or an instance of a subclass of
namedtuple) to a tuple, even if the instance's __iter__
method has been changed. Useful for writing derived instances of
typeclasses.
Args:
nt: an instance of namedtuple
Returns:
A tuple containing each of the items in nt
"""
return tuple((getattr(nt, f) for f in nt.__class__._fields))
class TypeMeta(type):
"""
Metaclass for Typeclass type. Ensures that all typeclasses are instantiated
with a dictionary to map instances to their member functions, and a list of
dependencies.
"""
def __init__(self, *args):
super(TypeMeta, self).__init__(*args)
self.__instances__ = {}
self.__dependencies__ = self.mro()[1:-2] # excl self, Typeclass, object
def __getitem__(self, item):
try:
if isinstance(item, ADT):
return self.__instances__[id(item.__type_constructor__)]
elif isinstance(typeof(item), ListType):
return self.__instances__[id(type(item))]
elif isinstance(item, Hask):
return self.__instances__[id(typeof(item))]
return self.__instances__[id(type(item))]
except KeyError:
raise TypeError("No instance for {0}".format(item))
class Typeclass(metaclass(TypeMeta)):
"""
Base class for Hask typeclasses.
All subclasses should implement make_instance, which controls what happens
when a new instance is added. This method should set up whatever
attributes/functions belong to the typeclass, and then call build_instance.
See typeclasses.py for examples.
"""
@classmethod
def make_instance(typeclass, type_, *args):
raise NotImplementedError("Typeclasses must implement make_instance")
@classmethod
def derive_instance(typeclass, type_):
raise NotImplementedError("Typeclasses must implement derive_instance")
def build_instance(typeclass, cls, attrs):
"""
Add a new instance to a typeclass, i.e. modify the typeclass's instance
dictionary to include the new instance.
Args:
typeclass: The typeclass for which we are adding an instance
cls: The class or type to be added
attrs: A dict of {str:function}, mapping function names to functions
for the typeclass instance
Returns: None
Raises:
TypeError, if cls is not a member of all superclasses of typeclass
"""
# 1) check dependencies
for dep in typeclass.__dependencies__:
if id(cls) not in dep.__instances__:
raise TypeError("Missing dependency: %s" % dep.__name__)
# 2) add type and its instance method to typeclass's instance dictionary
__methods__ = namedtuple("__%s__" % str(id(cls)), attrs.keys())(**attrs)
typeclass.__instances__[id(cls)] = __methods__
def has_instance(cls, typeclass):
"""
Test whether a class is a member of a particular typeclass.
Args:
cls: The class or type to test for membership
typeclass: The typeclass to check. Must be a subclass of Typeclass.
Returns:
True if cls is a member of typeclass, and False otherwise.
"""
if not issubclass(typeclass, Typeclass):
return False
return id(cls) in typeclass.__instances__
[docs]class Hask(object):
"""Base class for objects within hask.
`ADTs <ADT>`:class:, `TypedFunc`:class:,
`~hask.lang.lazylist.List`:class:, `Undefined`:class:, and other
hask-related types are all subclasses of Hask.
All subclasses must define ``__type__``, which returns a representation of
the object in the internal type system language.
"""
def __type__(self):
raise TypeError()
[docs]class Undefined(Hask):
"""
A class with no concrete type definition (so its type can unify with any
other type). Used to create `undefined` and to enable psuedo-laziness in
pattern matching.
"""
def __type__(self):
return TypeVariable()
[docs]class PyFunc(object):
"""
Singleton object that represents (any of the) Python function types in the
type system and in type signatures.
"""
pass
[docs]def typeof(obj):
"""Returns the type of an object within the internal type system.
:param obj: the object to inspect
:returns: An object representing the type in the internal type system
language (i.e., a `~hask.lang.hindley_milner.TypeOperator`:class: or
`~hask.lang.hindley_milner.TypeVariable`:class:).
"""
TypeVariable.next_var_name = 'a'
if isinstance(obj, Hask):
return obj.__type__()
elif isinstance(obj, tuple):
return Tuple([typeof(o) for o in obj])
elif obj is None:
return TypeOperator(None, [])
elif type(obj) in __python_function_types__:
return TypeOperator(PyFunc, [])
return TypeOperator(type(obj), [])
[docs]class TypeSignature(object):
"""Internal representation of a type signature, consisting of a list of
function type arguments and a list of (typeclass, type_variable) typeclass
constraint pairs.
"""
def __init__(self, args, constraints):
self.args = args
self.constraints = constraints
[docs]class TypeSignatureHKT(object):
"""Internal representation of a higher-kinded type within a type signature,
consisting of the type constructor and its type parameter names.
"""
def __init__(self, tcon, params):
self.tcon = tcon
self.params = params
[docs]class TypeSignatureError(Exception):
pass
def build_sig_arg(arg, cons, var_dict):
"""
Covert a single argument of a type signature into its internal type system
representation.
Args:
arg: The argument (a string, a Python type, etc) to convert
cons: a dictionary of typeclass constraints for the type signature
var_dict: a dictionary of bound type variables
Returns: A TypeVariable or TypeOperator representing the arg
Raises: TypeSignatureError, if the argument cannot be converted
"""
# string representing type variable
if isinstance(arg, str) and all((l in lowercase for l in arg)):
if arg not in var_dict:
if arg in cons:
var_dict[arg] = TypeVariable(constraints=cons[arg])
else:
var_dict[arg] = TypeVariable()
return var_dict[arg]
# subsignature, e.g. H/ (H/ int >> int) >> int >> int
elif isinstance(arg, TypeSignature):
return make_fn_type(build_sig(arg, var_dict))
# HKT, e.g. t(Maybe "a") or t("m", "a", "b")
elif isinstance(arg, TypeSignatureHKT):
if type(arg.tcon) == str:
hkt = build_sig_arg(arg.tcon, cons, var_dict)
else:
hkt = arg.tcon
return TypeOperator(hkt, [build_sig_arg(a, cons, var_dict)
for a in arg.params])
# None (the unit type)
elif arg is None:
return TypeOperator(None, [])
# Tuples: ("a", "b"), (int, ("a", float)), etc.
elif isinstance(arg, tuple):
f = lambda x: build_sig_arg(x, cons, var_dict)
return Tuple([f(x) for x in arg])
# Lists: ["a"], [int], etc.
elif isinstance(arg, list) and len(arg) == 1:
return ListType(build_sig_arg(arg[0], cons, var_dict))
# any other type, builtin or user-defined
elif isinstance(arg, type):
return TypeOperator(arg, [])
raise TypeSignatureError("Invalid item in type signature: %s" % arg)
def make_fn_type(params):
"""
Turn a list of type parameters into the corresponding internal type system
object that represents the type of a function over the parameters.
Args:
params: a list of type paramaters, e.g. from a type signature. These
should be instances of TypeOperator or TypeVariable
Returns:
An instance of TypeOperator representing the function type
"""
if len(params) == 2:
last_input, return_type = params
return Function(last_input, return_type)
return Function(params[0], make_fn_type(params[1:]))
def build_sig(type_signature, var_dict=None):
"""
Parse a TypeSignature object and convert it to the internal type system
language.
Args:
type_signature: an instance of TypeSignature
var_dict: a dictionary of already-bound type variables, or None
Returns: A list of TypeVariable/TypeOperator objects, representing the
function type corresponding to the type signature
"""
args = type_signature.args
cons = type_signature.constraints
var_dict = {} if var_dict is None else var_dict
return [build_sig_arg(i, cons, var_dict) for i in args]
[docs]class TypedFunc(Hask):
"""Partially applied, statically typed function wrapper.
"""
def __init__(self, fn, fn_args, fn_type):
self.__doc__ = fn.__doc__
self.func = fn
self.fn_args = fn_args
self.fn_type = fn_type
def __type__(self):
return self.fn_type
def __call__(self, *args, **kwargs):
# the environment contains the type of the function and the types
# of the arguments
env = {id(self): self.fn_type}
env.update({id(arg): typeof(arg) for arg in args})
ap = Var(id(self))
for arg in args:
if isinstance(arg, Undefined):
return arg
ap = App(ap, Var(id(arg)))
result_type = analyze(ap, env)
if len(self.fn_args) - 1 == len(args):
result = self.func(*args)
unify(result_type, typeof(result))
return result
return TypedFunc(functools.partial(self.func, *args, **kwargs),
self.fn_args[len(args):], result_type)
def __mod__(self, arg):
"""
(%) :: (a -> b) -> a -> b
% is the apply operator, equivalent to $ in Haskell
"""
return self.__call__(arg)
def __mul__(self, fn):
"""
(*) :: (b -> c) -> (a -> b) -> (a -> c)
* is the function compose operator, equivalent to . in Haskell
"""
if not isinstance(fn, TypedFunc):
return fn.__rmul__(self)
env = {id(self): self.fn_type, id(fn): fn.fn_type}
compose = Lam("arg", App(Var(id(self)), App(Var(id(fn)), Var("arg"))))
newtype = analyze(compose, env)
composed_fn = lambda x: self.func(fn.func(x))
newargs = [fn.fn_args[0]] + self.fn_args[1:]
return TypedFunc(composed_fn, fn_args=newargs, fn_type=newtype)
[docs]class ADT(Hask):
"""Base class for Hask algebraic data types."""
pass
def make_type_const(name, typeargs):
"""
Build a new type constructor given a name and the type parameters.
Args:
name: the name of the new type constructor to be created
typeargs: the type parameters to the constructor
Returns:
A new class that acts as a type constructor
"""
def raise_fn(err):
raise err()
default_attrs = {"__params__": tuple(typeargs), "__constructors__": ()}
cls = type(name, (ADT,), default_attrs)
cls.__type__ = lambda self: \
TypeOperator(cls, [TypeVariable() for i in cls.__params__])
# Unless typeclass instances are provided or derived, ADTs do not support
# any of these attributes, and trying to use one is a TypeError
cls.__iter__ = lambda self: raise_fn(TypeError)
cls.__contains__ = lambda self, other: raise_fn(TypeError)
cls.__add__ = lambda self, other: raise_fn(TypeError)
cls.__radd__ = lambda self, other: raise_fn(TypeError)
cls.__rmul__ = lambda self, other: raise_fn(TypeError)
cls.__mul__ = lambda self, other: raise_fn(TypeError)
cls.__lt__ = lambda self, other: raise_fn(TypeError)
cls.__gt__ = lambda self, other: raise_fn(TypeError)
cls.__le__ = lambda self, other: raise_fn(TypeError)
cls.__ge__ = lambda self, other: raise_fn(TypeError)
cls.__eq__ = lambda self, other: raise_fn(TypeError)
cls.__ne__ = lambda self, other: raise_fn(TypeError)
cls.count = lambda self, other: raise_fn(TypeError)
cls.index = lambda self, other: raise_fn(TypeError)
# Unless Show is instantiated/derived, use object's `repr` method
cls.__repr__ = object.__repr__
cls.__str__ = object.__str__
return cls
def make_data_const(name, fields, type_constructor, slot_num):
"""
Build a data constructor given the name, the list of field types, and the
corresponding type constructor.
The general approach is to create a subclass of the type constructor and a
new class created with `namedtuple`, with some of the features from
`namedtuple` such as equality and comparison operators stripped out.
Args:
name: the name of the data constructor (a string)
fields: the list of fields that the data constructor will have (a list
of strings and types)
type_constructor: the type constructor for the data constructor's type
slot_num: the position of the data constructor in the `data` statement
defining the new type (e.g., Nothing=0, Just=1)
Returns:
The new data constructor
"""
# create the data constructor
base = namedtuple(name, ["i%s" % i for i, _ in enumerate(fields)])
cls = type(name, (type_constructor, base), {})
cls.__type_constructor__ = type_constructor
cls.__ADT_slot__ = slot_num
if len(fields) == 0:
# If the data constructor takes no arguments, create an instance of it
# and return that instance rather than returning the class
cls = cls()
else:
# Otherwise, modify __type__ so that it matches up fields from the data
# constructor with type params from the type constructor
def __type__(self):
args = [
typeof(self[fields.index(p)]) if p in fields else TypeVariable()
for p in type_constructor.__params__
]
return TypeOperator(type_constructor, args)
cls.__type__ = __type__
type_constructor.__constructors__ += (cls,)
return cls
def build_ADT(typename, typeargs, data_constructors, to_derive):
"""
Create a new algebraic data type (a type constructor and at least one data
constructor).
Args:
typename: a string representing the name of the type constructor
typeargs: strings representing the type parameters of the type
constructor (should be unique, lowercase strings)
data_constructors: a list of (name, [field]) pairs representing
each of the data constructors for the new type.
to_derive: a list of typeclasses (subclasses of Typeclass) that should
be derived for the new type
Returns:
The type constructor, followed by each of the data constructors (in the
order they were defined)
Example usage:
build_ADT(typename="Maybe",
typeargs=["a"],
data_constructors=[("Nothing", []), ("Just", ["a"])],
to_derive=[Read, Show, Eq, Ord])
"""
# 1) Create the new type constructor and data constructors
newtype = make_type_const(typename, typeargs)
dcons = [make_data_const(dc_name, dc_fields, newtype, i)
for i, (dc_name, dc_fields) in enumerate(data_constructors)]
# 2) Derive typeclass instances for the new type constructor
for tclass in to_derive:
tclass.derive_instance(newtype)
# 3) Wrap data constructors in TypedFunc instances
for i, (dc_name, dc_fields) in enumerate(data_constructors):
if len(dc_fields) == 0:
continue
return_type = TypeSignatureHKT(newtype, typeargs)
sig = TypeSignature(list(dc_fields) + [return_type], [])
sig_args = build_sig(sig, {})
dcons[i] = TypedFunc(dcons[i], sig_args, make_fn_type(sig_args))
return tuple([newtype, ] + dcons)
class PatternMatchBind(object):
"""Represents a local variable bound by pattern matching."""
def __init__(self, name):
self.name = name
class PatternMatchListBind(object):
"""
Represents the head (first element) and tail (remaining elements) of a
pattern-matched list
"""
def __init__(self, head, tail):
self.head = head
self.tail = tail
def pattern_match(value, pattern, env=None):
"""Pattern match a value and a pattern.
:param value: the value to pattern-match on
:param pattern: a pattern, consisting of literals and/or locally bound
variables
:param env: a dictionary of local variables bound while matching
:returns: (True, env) if the match is successful, and (False, env)
otherwise
:raises SyntaxError: if a variable name is used multiple times in the same
pattern
"""
env = {} if env is None else env
if isinstance(pattern, PatternMatchBind):
if pattern.name in env:
raise SyntaxError("Conflicting definitions for %s" % pattern.name)
env[pattern.name] = value
return True, env
elif isinstance(pattern, PatternMatchListBind):
head, tail = list(value[:len(pattern.head)]), value[len(pattern.head):]
matches, env = pattern_match(head, pattern.head, env)
if matches:
return pattern_match(tail, pattern.tail, env)
return False, env
elif type(value) == type(pattern):
if isinstance(value, ADT):
return pattern_match(nt_to_tuple(value), nt_to_tuple(pattern), env)
elif is_collection(value):
matches = []
if len(value) != len(pattern):
return False, env
for v, p in zip(value, pattern):
match_status, env = pattern_match(v, p, env)
matches.append(match_status)
return all(matches), env
elif value == pattern:
return True, env
return False, env