0

I'm trying to define a generic base class that can instantiate wrapped instances of itself like so

from typing import Callable, TypeVar, Generic
T = TypeVar("T")
U = TypeVar("U")
class From(Generic[T]):
 def __init__(self, val: T):
 self.val = val
 
 @classmethod
 def unit(cls, val):
 return cls(val)
 
 def bind(self, func: Callable[[T], U]):
 return self.unit(func(self.val))

Basically, when calling bind on a From instance with a function that changes the type, e.g.

def to_string(i: int) -> str: 
 return str(i)
From(1).bind(to_string) # here, self.val = "1"

The type checker will complain because cls in From.unit is already parametrized by T (hence passing U to From[T] raises that U is not assignable to T )

You can overcome this problem by returning a parametrized instance of From in the unit method like so:

class From(Generic[T]):
 ...
 @classmethod
 def unit(cls, val: U) -> "From[U]":
 return From[U](val)

which works like a charm.

However, the problem occurs when I let another class inherit from From. I lose generality:

class Just(From[T]):
 pass
Just(1).bind(to_string) # equals From("1"), instead of Just("1")

Not accepting the solution that you should define a new unit method with the same form and class name on every inheriting instance, is there some way to re-parametrize the type of the cls variable in the original unit method?

I know you can use cls.__orig_bases__ to find (Generic[~T]) if you print it inside the unit method.

So I thought maybe you can do something like setattr(cls, "__orig_bases__", (Generic[U],))

But this seems senseless and in case is not picked up by the type checker.

Ideally there should be some method on self that allows you to access the unparametrized base class, so you could self.<get_baseclass>[U](<val>) .

Right now my solution is adding # type: ignore in unit like so:

@classmethod
def unit(cls, val: U) -> "From[U]":
 """Return a new instance of the same encapsulating class, wrapping `val`"""
 return cls(val) # type: ignore

But this is not ideal.

InSync
12.2k5 gold badges22 silver badges60 bronze badges
asked Dec 12, 2024 at 11:00
1
  • This is a common question; you need a higher kinded type, but there's no such thing in Python's type system just yet. There should be a few duplicates somewhere. Commented Dec 12, 2024 at 19:06

1 Answer 1

0

Use Self as the return type for both unit and bind.

class From(Generic[T]):
 def __init__(self, val: T):
 self.val = val
 @classmethod
 def unit(cls, val) -> Self: # returns an *instance* of cls
 return cls(val)
 def bind(self, func: Callable[[T], U]) -> Self:
 return self.unit(func(self.val))

Now reveal_type(Just(1).bind(to_string)) returns Just[int] as desired.

answered Dec 14, 2024 at 15:19
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.