Source code for hask.lang.type_system

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