I've been working on a project in python, and I needed to represent a certain numeric type. I need to represent the concept of some or all. Then I need to be able to add these amounts.
Coming to this juncture I found myself really wishing I was writing in Haskell. In Haskell I could build myself a nice little functor like so:
data Amount = All | Some Integer deriving (Eq, Show)
instance Num Amount where
All + _ = All
_ + All = All
(Some a) + (Some b) = Some (a + b)
(This gives a warning because Amount
is not a complete instance of Num
however the point here is just a minimal demonstration)
I could even use Maybe
s instead of creating my own datatype:
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
import Control.Applicative (liftA2)
type Amount = Maybe Integer
instance Num Amount where
(+) = liftA2 (+)
This second solution isn't as clean, it requires some language extensions and uses the word Nothing
instead of All
which could be confusing, but it is still an OK solution in my book.
This is a very easy task in Haskell with solutions I consider elegant and clear. However in Python there are no clean solutions that I can come up with. Every solution leaves me with a bad taste in my mouth. Here are the 4 solutions I've come up with:
1
class Amount(object):
def __init__(self,repr):
self.repr = repr
def __add__(self,other):
if self.repr == "all" or other.repr == "all":
return Amount("all")
else:
return Amount(self.repr + other.repr)
1.1
class Amount(object):
def __init__(self,repr):
if repr == "all" or type(repr) is int:
self.repr = repr
else:
raise Exception('Amount must be either an int or "all".')
def __add__(self,other):
if self.repr == "all" or other.repr == "all":
return Amount("all")
elif type(self.repr) is type(other.repr) is int:
return Amount(self.repr + other.repr)
else:
raise Exception('Attempted to add malformed Amounts.')
2
class Amount(object):
def __init__(self,isAll,value):
self.isAll = isAll
self.value = value
def __add__(self,other):
return Amount(self.isAll or other.isAll, self.value + other.value)
2.1
class Amount(object):
def __init__(self,isAll,value):
self.isAll = isAll
if self.isAll:
self.value = 0
else:
self.value = value
def __add__(self,other):
if self.isAll or other.isAll:
return Amount(True, 0)
else:
return Amount(False, self.value + other.value)
3
class All(object):
def __init__(self):pass
def __add__(self, other):
return All()
class Some(object):
def __init__(self, quantity):
self.quantity = quantity
def __add__(self, other):
if type(other) is Some:
return Some(self.quantity + other.quantity)
elif type(other) is All:
return All()
else:
return Some(other + self.quantity)
4
class All(object):
def __init__(self):pass
def __add__(self, other):
return All()
Each solution has its own problems. The first solutions 1 and 1.1 are basically stringly typed solutions our class is a container which contains either a string or a integer. Solution 1.1 adds some safety by checking things but overall I find this solution very hacky.
The second solution is pretty much a union type with two parameters. It is much less hackish than the first, and is actually a solution I have seen other people use to similar problems in the past, but I still feel that it is a pretty bad fit. My big problem is that there is always extra data hanging around. Even though it isn't very much data and doesn't really cause any bloat, this makes me uncomfortable that a malformed function or method might try accidentally use the value
property of an All
, leading issues that may be very difficult to debug. 2.1 attempts to fix this by constantly setting the value to 0 when isAll
is on, but I still feel am not very satisfied. Overall I feel that this solution is also pretty hackish.
The third solution implements two different types, one for All
and one for Some
. This is probably my favorite solution since it kind of approximates my Haskell solution but it still has issues. The big issue is that Some
and All
are not the same type, they are different. I'm not a big fan of mixing types and any time I perform a type check I'm going to have the issue that these things that are meant to go together have different types.
The final solution is similar to the last except it gets rid of the Some
type. This is nice in that it behaves much in the way the third solution does but is much slimmer. That being said it alleviates none of the existing issues with the last solution
Which of these solutions is best and why?
1 Answer 1
isinstance
should be used rather thantype() is
.- You should define a
__repr__
or__str__
on your classes. - Your last example doesn't work if you use
1 + All()
, you also have to define__radd__
.
And so changing the above gets the following usages:
print(Amount('all') + Amount('all'))
print(Amount('all') + Amount(1))
print(Amount(1) + Amount('all'))
print(Amount(1) + Amount(1))
print(Amount(True, 0) + Amount(True, 0))
print(Amount(True, 0) + Amount(False, 1))
print(Amount(False, 1) + Amount(True, 0))
print(Amount(False, 1) + Amount(False, 1))
print(All() + All())
print(All() + Some(1))
print(Some(1) + All())
print(Some(1) + Some(1))
print(All() + All())
print(All() + 1)
print(1 + All())
print(1 + 1)
- To keep with Haskell I would use the third option.
- I would also make an
Amount
class so that the way the objects interact is standardized. - I'd make
All
an instance, so that you don't have to constantly call it, or useisinstance
.
class Amount:
def __init__(self, value):
self._value = value
def __add__(self, other):
if self is All or other is All:
return All
else:
return Some(self._value + other._value)
class Some(Amount):
def __repr__(self):
return 'Some({})'.format(self.value)
class All(Amount):
def __repr__(self):
return 'All'
All = All(None)
print(All + All)
print(All + Some(1))
print(Some(1) + All)
print(Some(1) + Some(1))
Explore related questions
See similar questions with these tags.
None
is a good candidate for yourAll
(akin toMaybe
solution). That said, I am afraid that the question is off-topic here. \$\endgroup\$