ekampf / pymaybe

A Python implementation of the Maybe pattern
BSD 3-Clause "New" or "Revised" License
208 stars 6 forks source link

Allow setting or_else() value in advance #2

Closed sametmax closed 8 years ago

sametmax commented 8 years ago

For some of my work, I'm considering exposing some objects as Maybe objects instead of just dict like objects. In some specific cases I'll need to provide and or_else() value to the user so he or she doesn't have to set it him or herself.

We could imagine something like:

(test) >>> print(maybe('foo').bar.or_else())
None
(test) >>> print(maybe('foo', or_else="wololo").bar.or_else())
wololo

This way I can even abstract this part of the process, but still allow a user to override it if needed.

What do you think ?

ekampf commented 8 years ago

Hey @sametmax,

I thought about what you're trying to achieve and I think it misses the point of Something and Nothing adding another value (or_else) they have to carry with them.

I thought maybe you can use a descriptor. Define an object that has a descriptor that uses maybe and encapsulates the default value.

Here's an example. Say you're reading videos info from YouTube:

youtube_video_json_string = """
{"snippet": {"playlistId": "UU6lVC6W94JnpNrBKWEeQfyQ", "thumbnails": {"default": {"url": "https://i.ytimg.com/vi/ujY6nwmpevo/default.jpg", "width": 120, "height": 90}, "high": {"url": "https://i.ytimg.com/vi/ujY6nwmpevo/hqdefault.jpg", "width": 480, "height": 360}, "medium": {"url": "https://i.ytimg.com/vi/ujY6nwmpevo/mqdefault.jpg", "width": 320, "height": 180}, "maxres": {"url": "https://i.ytimg.com/vi/ujY6nwmpevo/maxresdefault.jpg", "width": 1280, "height": 720}, "standard": {"url": "https://i.ytimg.com/vi/ujY6nwmpevo/sddefault.jpg", "width": 640, "height": 480}}, "title": "Stocks App Take I", "resourceId": {"kind": "youtube#video", "videoId": "ujY6nwmpevo"}, "channelId": "UC6lVC6W94JnpNrBKWEeQfyQ", "publishedAt": "2014-10-25T14:01:06.000Z", "channelTitle": "Eran Kampf", "position": 0, "description": "Early take of Stocks"}, "kind": "youtube#playlistItem", "etag": "iDqJ1j7zKs4x3o3ZsFlBOwgWAHU/46J7teBS1LTh87m1e4lwFDavz4E", "id": "UUwuIl3s_oR92MeZOEXU9uq7fuRwnm09L5"}
"""

You'll probably want to encapsulate that response to an object:

class YoutubeResponse(object):
    def __init__(self, json_string):
        self._json = json.loads(json_string)

    id = MaybeDescriptor('_json', 'id')
    title = MaybeDescriptor('_json', 'snippet', 'title')
    description = MaybeDescriptor('_json', 'snippet', 'description', default='no description...')

This way you pass around nice objects instead of dicts, and you encapsulate the _orelse information you wanted.

The code for MaybeDescriptor (which I'm considering adding to pymaybe):

class MaybeDescriptor(object):
    def __init__(self, *path, default=None):
        self.path = path
        self.default_value = default

    def __get__(self, instance, owner):
        maybe_value = maybe(instance)
        for p in self.path:
            maybe_value = getattr(maybe_value, p) or maybe_value[p]

        return maybe_value.or_else(self.default_value)

What do you think about this solution?

EDIT: Having to put 'json' as the first argument of each Descriptor is annoying. I'm thinking of setting a convention that its always called root and Descriptor will always use __root as first argument

ekampf commented 8 years ago
class MaybeObject():
    def __init__(self, data):
        self.__data

class MaybeDescriptor(object):
    def __init__(self, *path, default=None):
        self.path = path
        self.default_value = default

    def __get__(self, instance, owner):
        maybe_value = maybe(instance)
        maybe_value = getattr(maybe_value, '__data')
        for p in self.path:
            maybe_value = getattr(maybe_value, p) or maybe_value[p]

        return maybe_value.or_else(self.default_value)

class YoutubeResponse(object):
    id = MaybeDescriptor('id')
    title = MaybeDescriptor('snippet', 'title')
    description = MaybeDescriptor('snippet', 'description', default='no description...')
ekampf commented 8 years ago

Then again maybe you're just better off doing initialisation of values explicitly in init and not lazily via descriptor:

class YoutubeResponse(object):
    def __init__(self, data):
        self.id = maybe(data)['id'].get()
        self.title = maybe(data)['snippet']['title'].get()
        self.description = maybe(data)['snippet']['description'].or_else('no description...')
sametmax commented 8 years ago

That would require to craft every object in a way that it accommodate the use of maybe (hence tying them to it) VS just using maybe when some more general part of my program use the object.

ekampf commented 8 years ago

Storing another value to be carried on down the call stack with the Something\Nothing objects is kinda messy and defeats the purpose of single responsibility of Something and Nothing. They should represent just that a value or a non-value... and not carry around an extra value.

The best way to implement this functionality is to have another object that wraps this functionality. An Optional (or maybe a Promise is a better name?) class that wraps a maybe() instance and a default. Something like

class Promise(object):
   def __init__(self, value, default=None):
      self.__value = maybe(value)  # just in case, to make sure value is Something\Nothing
      self.__default = default

  def get(self):
      return self.__value.or_else(self.__default)
sametmax commented 8 years ago

That makes sense.