1
\$\begingroup\$

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 Maybes 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?

\$\endgroup\$
  • \$\begingroup\$ If I understand you correctly, None is a good candidate for your All (akin to Maybe solution). That said, I am afraid that the question is off-topic here. \$\endgroup\$ – vnp Apr 11 '18 at 0:43
  • \$\begingroup\$ @vnp Can I ask why it is off topic? I read through the help center before asking (and again just now) and it seemed to be on topic by all the criteria listed there. \$\endgroup\$ – Sriotchilism O'Zaic Apr 11 '18 at 0:48
  • 2
    \$\begingroup\$ A gut feeling: we are not asked to review, but to suggest something radically different. I think SO is really a better place. \$\endgroup\$ – vnp Apr 11 '18 at 0:49
  • \$\begingroup\$ You're offering varying degrees of code and it's unclear what you're using and therefor difficult to review. Also, I'm not a pro, but it seems like you're talking about two different languages \$\endgroup\$ – Raystafarian Apr 11 '18 at 5:50
  • \$\begingroup\$ @vnp I've gone ahead and given the question a makeover. Hopefully now it is not asking for a solution other than the ones I have provided. \$\endgroup\$ – Sriotchilism O'Zaic May 10 '18 at 19:21
1
\$\begingroup\$
  1. isinstance should be used rather than type() is.
  2. You should define a __repr__ or __str__ on your classes.
  3. 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)
  1. To keep with Haskell I would use the third option.
  2. I would also make an Amount class so that the way the objects interact is standardized.
  3. I'd make All an instance, so that you don't have to constantly call it, or use isinstance.
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))
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.