[Types-sig] A challenge

Tony Lownds tony@metanet.com
Fri, 17 Dec 1999 10:37:06 -0800 (PST)

Here is another syntactical variant. 

import sys, find

def main() -> None:
    #Note:  I have to rename the "list" variable here, because list is
    # used as a type
    a_list: list of str
    name: str
    dir:= "."		#Note: type implied from the literal. 
    if sys.argv[1:]:
        dir = sys.argv[1]
    a_list = find.find("*.py", dir)
    for name in a_list:
        print name

if __name__ == "__main__":

import fnmatch
import os
_debug: = 0
_prune: list of string = ['(*)']

def find(pattern: str, dir: str = os.curdir) -> list of str:
    #Note: again, renaming the var named "list"
    a_list: list of str = []
    names:= os.listdir(dir)	#Note: asking for type to be implied here
    for name:str in names:	
        if name in (os.curdir, os.pardir):
        #Note: not asking for fullname to be typed; its usage should be 
        # easy to type check
        fullname = os.path.join(dir, name)
        if fnmatch.fnmatch(name, pattern):
        if os.path.isdir(fullname) and not os.path.islink(fullname):
            for p:str in _prune:
                if fnmatch.fnmatch(name, p):
                    if _debug: print "skip", `fullname`
                if _debug: print "descend into", `fullname`
                a_list = a_list + find(pattern, fullname)
    return a_list

import re

#Note: I'm showing dictionaries' types are declared using a literal (ie
# {str: int})  rather than a parameterized type name (list of int) because
# a) the consistent choice for the name of a dictionary ("dict") doesnt
#    exist in python right now
# b) actually tuples and lists can be declared using a literal, but that
# would declare a tuple/list of exactly that size.
# also note that RegexObject is in a module and is accessed as such.
_cache:{str:re.RegexObject} = {}

# Declaring all the function signatures in a block here, to follow Tim's
# format. 
# The reason that "declare" is being used is that if I simply declared the
# type of the variable just like the other local variable then when the
# type checker gets to the actual def statement, which is really just an
# assignment, it should raise an error if it cant determine that the new
# value assigned does not match the definition. 
# Also I am assuming a "bool" type. The corresponding builtin
# function "bool" (str, int, float, etc. all have corresponding builtin
# functions and I think that is a Good Thing) could in essence be:
# def bool(value):
#   if any: 
#     return 1
#   else: 
#     return 0

  fnmatch: (str, str) -> bool
  fnmatchcase: (str, str) -> bool
  translate: str -> str

# showing this one again with parameter names; without them you
# restrict the users of this function from using a keyword calling syntax.
  fnmatchcase: (name:str, pat:str) -> bool

def fnmatch(name, pat):
    import os
    name = os.path.normcase(name)
    pat = os.path.normcase(pat)
    return fnmatchcase(name, pat)

def fnmatchcase(name, pat):
    if not _cache.has_key(pat):
        res:str = translate(pat)
        _cache[pat] = re.compile(res)
    return _cache[pat].match(name) is not None

def translate(pat):
    #Note: the next line was originally:   i, n = 0, len(pat)
    # I had to add parens to use the implied type sugar and its not as
    # easy to read. Which makes me wonder if that bit of sugar is a wart.
    (i, n) := 0, len(pat)
    res := ''
    while i < n:
        # Note: introducing chr as a type
        c:chr = pat[i]
        i = i+1
        if c == '*':
            res = res + '.*'
        elif c == '?':
            res = res + '.'
        elif c == '[':
            j:int = i
            if j < n and pat[j] == '!':
                j = j+1
            if j < n and pat[j] == ']':
                j = j+1
            while j < n and pat[j] != ']':
                j = j+1
            if j >= n:
                res = res + '\\['
                stuff:str = pat[i:j]
                i = j+1
                if stuff[0] == '!':
                    stuff = '[^' + stuff[1:] + ']'
                elif stuff == '^'*len(stuff):
                    stuff = '\\^'
                    while stuff[0] == '^':
                        stuff = stuff[1:] + stuff[0]
                    stuff = '[' + stuff + ']'
                res = res + stuff
            res = res + re.escape(c)
    return res + "$"

Thats it. 

Here is a point-by-point summary, so you can quickly point out what you
dislike (and if a few people do so off-line then I'll post a summary
and maybe save a bit of traffic):

- types are either classes or builtin type names or type expressions.
- the builtin functions involving types are overloaded when used in type
- "bool" is a new type and a new builtin
- parameterized types are instantiated (er, whats the real term for this?)
  with the "of" operator.
- dictionaries of any size are defined by a literal dictionary syntax
- lists and tuples of exact size are defined by a literal tuple syntax
- callables are shown with the -> operator 
- the arguments to a signature is not a tuple; you can have names and
  * and ** keywords.
- you can omit the type expression altogether if it is an assignment from
  a well-known function (e.g. builtin) or a simple literal (e.g. 0, '')
- variables created by for loops are typed in-line
- if you are specifying a signature for a function that occurs later it
  should be in a declare block
- using "list", "int", etc. for variable names potentially shadows the
  builtin function and the type name.