pythontypeclasspython-modulepython-typingabc

Implement a type in Python, for functions in a module (not class)?


def f(x, y):
    return x & 1 == 0 and y > 0

g = lambda x, y: x & 1 == 0 and y > 0

Now the same thing in Haskell:

import Data.Bits

f :: Int -> Int -> Bool
f x y = (.&.) x 1 == 0 && y > 0

That works, however this doesn't:

g = \x y -> (.&.) x 1 == 0 && y > 0

Here's the error this gives:

someFunc :: IO ()
someFunc = putStrLn $ "f 5 7: " ++ ( show $ f 5 7 ) ++ "\tg 5 7: " ++ ( show $ g 5 7 )

• Ambiguous type variable ‘a0’ arising from the literal ‘1’
  prevents the constraint ‘(Num a0)’ from being solved.
  Relevant bindings include
    x :: a0 (bound at src/Lib.hs:13:6)
    g :: a0 -> Integer -> Bool (bound at src/Lib.hs:13:1)
  Probable fix: use a type annotation to specify what ‘a0’ should be.
  These potential instances exist:
    instance Num Integer -- Defined in ‘GHC.Num’
    instance Num Double -- Defined in ‘GHC.Float’
    instance Num Float -- Defined in ‘GHC.Float’
    ...plus two others
    ...plus one instance involving out-of-scope types
    (use -fprint-potential-instances to see them all)
• In the second argument of ‘(.&.)’, namely ‘1’
  In the first argument of ‘(==)’, namely ‘(.&.) x 1’
  In the first argument of ‘(&&)’, namely ‘(.&.) x 1 == 0’
   |
13 | g = \x y -> (.&.) x 1 == 0 && y > 0
   |                     ^

How do I get the same error in Python? - How do I get errors when the input doesn't match expectations?

To be specific, how do I say that a function/lambda MUST have:

I know that I can roughly do this with: (docstrings and/or PEP484) with abc; for classes. But what can I do for 'loose' functions in a module?


Solution

  • One trick I found was to use Callback Protocols:

    from typing import Optional, Iterable
    from sys import version_info
    
    if version_info > (3, 8):
        from typing import Protocol
    else:
        from typing_extensions import Protocol
    
    
    class Combiner(Protocol):
        def __call__(self, *vals: bytes, maxlen: Optional[int] = None) -> list[bytes]: ...
    

    Applied to my problem, just duplicate:

    def combiner0(*vals: bytes, maxlen: Optional[int] = None) -> list[bytes]: return []
    def combiner1(*vals, maxlen = None): return []
    
    # Unused var just for type checking
    _combiner: Combiner = combiner0
    _combiner: Combiner = combiner1
    del _combiner