How can I write a function fmap that returns the same type of iterable that was inputted?
How can I write a function "fmap", with this properties :
>>> l = [1, 2]; fmap(lambda x: 2*x, l)
[2, 4]
>>> l = (1, 2); fmap(lambda x: 2*x, l)
(2, 4)
>>> l = {1, 2}; fmap(lambda x: 2*x, l)
{2, 4}
(I search a kind of "fmap" in haskell, but in python3).
I have a very ugly solution, but there is certainly a solution more pythonic and generic ? :
def fmap(f, container):
t = container.__class__.__name__
g = map(f, container)
return eval(f"{t}(g)")
python python-3.x functional-programming
add a comment |
How can I write a function "fmap", with this properties :
>>> l = [1, 2]; fmap(lambda x: 2*x, l)
[2, 4]
>>> l = (1, 2); fmap(lambda x: 2*x, l)
(2, 4)
>>> l = {1, 2}; fmap(lambda x: 2*x, l)
{2, 4}
(I search a kind of "fmap" in haskell, but in python3).
I have a very ugly solution, but there is certainly a solution more pythonic and generic ? :
def fmap(f, container):
t = container.__class__.__name__
g = map(f, container)
return eval(f"{t}(g)")
python python-3.x functional-programming
2
Breaks if:fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}).. and in any other case where the function changes the type...
– Patrick Artner
Jan 2 at 10:27
add a comment |
How can I write a function "fmap", with this properties :
>>> l = [1, 2]; fmap(lambda x: 2*x, l)
[2, 4]
>>> l = (1, 2); fmap(lambda x: 2*x, l)
(2, 4)
>>> l = {1, 2}; fmap(lambda x: 2*x, l)
{2, 4}
(I search a kind of "fmap" in haskell, but in python3).
I have a very ugly solution, but there is certainly a solution more pythonic and generic ? :
def fmap(f, container):
t = container.__class__.__name__
g = map(f, container)
return eval(f"{t}(g)")
python python-3.x functional-programming
How can I write a function "fmap", with this properties :
>>> l = [1, 2]; fmap(lambda x: 2*x, l)
[2, 4]
>>> l = (1, 2); fmap(lambda x: 2*x, l)
(2, 4)
>>> l = {1, 2}; fmap(lambda x: 2*x, l)
{2, 4}
(I search a kind of "fmap" in haskell, but in python3).
I have a very ugly solution, but there is certainly a solution more pythonic and generic ? :
def fmap(f, container):
t = container.__class__.__name__
g = map(f, container)
return eval(f"{t}(g)")
python python-3.x functional-programming
python python-3.x functional-programming
edited Jan 2 at 10:30
HNoob
asked Jan 2 at 10:09
HNoobHNoob
286312
286312
2
Breaks if:fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}).. and in any other case where the function changes the type...
– Patrick Artner
Jan 2 at 10:27
add a comment |
2
Breaks if:fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}).. and in any other case where the function changes the type...
– Patrick Artner
Jan 2 at 10:27
2
2
Breaks if:
fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}) .. and in any other case where the function changes the type...– Patrick Artner
Jan 2 at 10:27
Breaks if:
fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}) .. and in any other case where the function changes the type...– Patrick Artner
Jan 2 at 10:27
add a comment |
3 Answers
3
active
oldest
votes
Using the type of the input as a converter is not necessarily working in all occasions. map is just using the "iterability" of its input to produce its output. In Python3 this is why map returns a generator instead of a list (which is just more fitting).
So a cleaner and more robust version would be one which explicitly expects the various possible inputs it can handle and which raises an error in all other cases:
def class_retaining_map(fun, iterable):
if type(iterable) is list: # not using isinstance(), see below for reasoning
return [ fun(x) for x in iterable ]
elif type(iterable) is set:
return { fun(x) for x in iterable }
elif type(iterable) is dict:
return { k: fun(v) for k, v in iterable.items() }
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return { fun(k): v for k, v in iterable.items() }
else:
raise TypeError("type %r not supported" % type(iterable))
You could add a case for all other iterable values in the else clause of cause:
else:
return (fun(x) for x in iterable)
But that would e. g. return an iterable for a subclass of set which might not be what you want.
Mind that I'm deliberately not using isinstance because that would make a list out of a subclass of list, for instance. I figure that this is explicitly not wanted in this case.
One could argue that anything which is a list (i. e. is a subclass of list) needs to comply to have a constructor which returns a thing of this type for an iteration of elements. And likewise for subclasses of set, dict (which must work for an iteration of pairs), etc. Then the code might look like this:
def class_retaining_map(fun, iterable):
if isinstance(iterable, (list, set)):
return type(iterable)(fun(x) for x in iterable)
elif isinstance(iterable, dict):
return type(iterable)((k, fun(v)) for k, v in iterable.items())
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return type(iterable)((fun(k), v) for k, v in iterable.items())
else:
raise TypeError("type %r not supported" % type(iterable))
1
Are you deliberately usingtype(iterable) == listinstead ofisinstance? I can think of very few cases were this is not a bad practice. And even then, you would useisinstead of==.
– Eli Korvigo
Jan 2 at 10:40
@EliKorvigo Yes, I'm usingtype(iterable)deliberately (see my last paragraph which I added late for the reason of this). I changed the use of==to the use ofiswhich is slightly better in this case, you are right.
– Alfe
Jan 2 at 10:43
1
I'll adopt this solution, with another case :elif type(iterable) is str: return "".join(map(fun, iterable))
– HNoob
Jan 2 at 10:55
1
In case you need strings, you might want to consider to addbytesin Python3 orunicodein Python2.
– Alfe
Jan 2 at 10:58
add a comment |
Instantiate directly rather than via eval
__class__ can also be used to instantiate new instances:
def mymap(f, contener):
t = contener.__class__
return t(map(f, contener))
This removes the need for eval, use of which is considered poor practice. As per @EliKorvigo's comment, you may prefer built-in type to a magic method:
def mymap(f, contener):
t = type(contener)
return t(map(f, contener))
As explained here and in the docs:
The return value is a type object and generally the same object as returned by
object.__class__.
"Generally the same" should be considered "equivalent" in the case of new-style classes.
Testing for an iterable
You can check/test for an iterable in a couple of ways. Either use try / except to catch TypeError:
def mymap(f, contener):
try:
mapper = map(f, contener)
except TypeError:
return 'Input object is not iterable'
return type(contener)(mapper)
Or use collections.Iterable:
from collections import Iterable
def mymap(f, contener):
if isinstance(contener, Iterable):
return type(contener)(map(f, contener))
return 'Input object is not iterable'
This works specifically because built-in classes commonly used as containers such as list, set, tuple, collections.deque, etc, can be used to instantiate instances via a lazy iterable. Exceptions exist: for example, str(map(str.upper, 'hello')) will not work as you might expect even though str instances are iterable.
2
I'd say, callingtype(container)(map(...))is a bit cleaner than accessing a magic attribute.
– Eli Korvigo
Jan 2 at 10:22
1
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})
– Patrick Artner
Jan 2 at 10:33
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
1
Yourtry/exceptcode catches way too many cases. EachTypeErrorthrown in the execution offwithin themapcall will be caught and misinterpreted as "Input object is not iterable".
– Alfe
Jan 2 at 10:51
|
show 3 more comments
I search a kind of "fmap" in haskell, but in python3
First, let's discuss Haskell's fmap to understand, why it behaves the way it does, though I assume you are fairly familiar with Haskell considering the question. fmap is a generic method defined in the Functor type-class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
...
Functors obey several important mathematical laws and have several methods derived from fmap, though the latter is sufficient for a minimal complete functor instance. In other words, in Haskell types belonging to the Functor type-class implement their own fmap functions (moreover, Haskell types can have multiple Functor implementations via newtype definitions). In Python we don't have type-classes, though we do have classes that, while less convenient in this case, allow us to simulate this behaviour. Unfortunately, with classes we can't add functionality to an already defined class without subclassing, which limits our ability to implement a generic fmap for all builtin types, though we can overcome it by explicitly checking for acceptable iterable types in our fmap implementation. It's also literally impossible to express higher-kinded types using Python's type system, but I digress.
To summarise, we've got several options:
- Support all
Iterabletypes (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smapback into the original type. That is the duty of applying a function over the values inside a container is taken away from the container. This approach differs drastically from the functor interface: functors are supposed to handle the mapping themselves and handle additional metadata crucial to reconstruct the container. - Support a subset of readily mappable builtin iterable types (i.e. builtins that don't carry any important metadata). This solution is implemented by @Alfe, and, while less generic, it is safer.
- Take solution #2 and add support for proper user-defined functors.
This is my take on the third solution
import abc
from typing import Generic, TypeVar, Callable, Union,
Dict, List, Tuple, Set, Text
A = TypeVar('A')
B = TypeVar('B')
class Functor(Generic[A], metaclass=abc.ABCMeta):
@abc.abstractmethod
def fmap(self, f: Callable[[A], B]) -> 'Functor[B]':
raise NotImplemented
FMappable = Union[Functor, List, Tuple, Set, Dict, Text]
def fmap(f: Callable[[A], B], fmappable: FMappable) -> FMappable:
if isinstance(fmappable, Functor):
return fmappable.fmap(f)
if isinstance(fmappable, (List, Tuple, Set, Text)):
return type(fmappable)(map(f, fmappable))
if isinstance(fmappable, Dict):
return type(fmappable)(
(key, f(value)) for key, value in fmappable.items()
)
raise TypeError('argument fmappable is not an instance of FMappable')
Here is a demo
In [20]: import pandas as pd
In [21]: class FSeries(pd.Series, Functor):
...:
...: def fmap(self, f):
...: return self.apply(f).astype(self.dtype)
...:
In [22]: fmap(lambda x: x * 2, [1, 2, 3])
Out[22]: [2, 4, 6]
In [23]: fmap(lambda x: x * 2, {'one': 1, 'two': 2, 'three': 3})
Out[23]: {'one': 2, 'two': 4, 'three': 6}
In [24]: fmap(lambda x: x * 2, FSeries([1, 2, 3], index=['one', 'two', 'three']))
Out[24]:
one 2
two 4
three 6
dtype: int64
In [25]: fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-27-1c4524f8e4b1> in <module>
----> 1 fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
<ipython-input-7-53b2d5fda1bf> in fmap(f, fmappable)
34 if isinstance(fmappable, Functor):
35 return fmappable.fmap(f)
---> 36 raise TypeError('argument fmappable is not an instance of FMappable')
37
38
TypeError: argument fmappable is not an instance of FMappable
This solution allows us to define multiple functors for the same type via subclassing:
In [26]: class FDict(dict, Functor):
...:
...: def fmap(self, f):
...: return {f(key): value for key, value in self.items()}
...:
...:
In [27]: fmap(lambda x: x * 2, FDict({'one': 1, 'two': 2, 'three': 3}))
Out[27]: {'oneone': 1, 'twotwo': 2, 'threethree': 3}
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54004428%2fhow-can-i-write-a-function-fmap-that-returns-the-same-type-of-iterable-that-was%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
3 Answers
3
active
oldest
votes
3 Answers
3
active
oldest
votes
active
oldest
votes
active
oldest
votes
Using the type of the input as a converter is not necessarily working in all occasions. map is just using the "iterability" of its input to produce its output. In Python3 this is why map returns a generator instead of a list (which is just more fitting).
So a cleaner and more robust version would be one which explicitly expects the various possible inputs it can handle and which raises an error in all other cases:
def class_retaining_map(fun, iterable):
if type(iterable) is list: # not using isinstance(), see below for reasoning
return [ fun(x) for x in iterable ]
elif type(iterable) is set:
return { fun(x) for x in iterable }
elif type(iterable) is dict:
return { k: fun(v) for k, v in iterable.items() }
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return { fun(k): v for k, v in iterable.items() }
else:
raise TypeError("type %r not supported" % type(iterable))
You could add a case for all other iterable values in the else clause of cause:
else:
return (fun(x) for x in iterable)
But that would e. g. return an iterable for a subclass of set which might not be what you want.
Mind that I'm deliberately not using isinstance because that would make a list out of a subclass of list, for instance. I figure that this is explicitly not wanted in this case.
One could argue that anything which is a list (i. e. is a subclass of list) needs to comply to have a constructor which returns a thing of this type for an iteration of elements. And likewise for subclasses of set, dict (which must work for an iteration of pairs), etc. Then the code might look like this:
def class_retaining_map(fun, iterable):
if isinstance(iterable, (list, set)):
return type(iterable)(fun(x) for x in iterable)
elif isinstance(iterable, dict):
return type(iterable)((k, fun(v)) for k, v in iterable.items())
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return type(iterable)((fun(k), v) for k, v in iterable.items())
else:
raise TypeError("type %r not supported" % type(iterable))
1
Are you deliberately usingtype(iterable) == listinstead ofisinstance? I can think of very few cases were this is not a bad practice. And even then, you would useisinstead of==.
– Eli Korvigo
Jan 2 at 10:40
@EliKorvigo Yes, I'm usingtype(iterable)deliberately (see my last paragraph which I added late for the reason of this). I changed the use of==to the use ofiswhich is slightly better in this case, you are right.
– Alfe
Jan 2 at 10:43
1
I'll adopt this solution, with another case :elif type(iterable) is str: return "".join(map(fun, iterable))
– HNoob
Jan 2 at 10:55
1
In case you need strings, you might want to consider to addbytesin Python3 orunicodein Python2.
– Alfe
Jan 2 at 10:58
add a comment |
Using the type of the input as a converter is not necessarily working in all occasions. map is just using the "iterability" of its input to produce its output. In Python3 this is why map returns a generator instead of a list (which is just more fitting).
So a cleaner and more robust version would be one which explicitly expects the various possible inputs it can handle and which raises an error in all other cases:
def class_retaining_map(fun, iterable):
if type(iterable) is list: # not using isinstance(), see below for reasoning
return [ fun(x) for x in iterable ]
elif type(iterable) is set:
return { fun(x) for x in iterable }
elif type(iterable) is dict:
return { k: fun(v) for k, v in iterable.items() }
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return { fun(k): v for k, v in iterable.items() }
else:
raise TypeError("type %r not supported" % type(iterable))
You could add a case for all other iterable values in the else clause of cause:
else:
return (fun(x) for x in iterable)
But that would e. g. return an iterable for a subclass of set which might not be what you want.
Mind that I'm deliberately not using isinstance because that would make a list out of a subclass of list, for instance. I figure that this is explicitly not wanted in this case.
One could argue that anything which is a list (i. e. is a subclass of list) needs to comply to have a constructor which returns a thing of this type for an iteration of elements. And likewise for subclasses of set, dict (which must work for an iteration of pairs), etc. Then the code might look like this:
def class_retaining_map(fun, iterable):
if isinstance(iterable, (list, set)):
return type(iterable)(fun(x) for x in iterable)
elif isinstance(iterable, dict):
return type(iterable)((k, fun(v)) for k, v in iterable.items())
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return type(iterable)((fun(k), v) for k, v in iterable.items())
else:
raise TypeError("type %r not supported" % type(iterable))
1
Are you deliberately usingtype(iterable) == listinstead ofisinstance? I can think of very few cases were this is not a bad practice. And even then, you would useisinstead of==.
– Eli Korvigo
Jan 2 at 10:40
@EliKorvigo Yes, I'm usingtype(iterable)deliberately (see my last paragraph which I added late for the reason of this). I changed the use of==to the use ofiswhich is slightly better in this case, you are right.
– Alfe
Jan 2 at 10:43
1
I'll adopt this solution, with another case :elif type(iterable) is str: return "".join(map(fun, iterable))
– HNoob
Jan 2 at 10:55
1
In case you need strings, you might want to consider to addbytesin Python3 orunicodein Python2.
– Alfe
Jan 2 at 10:58
add a comment |
Using the type of the input as a converter is not necessarily working in all occasions. map is just using the "iterability" of its input to produce its output. In Python3 this is why map returns a generator instead of a list (which is just more fitting).
So a cleaner and more robust version would be one which explicitly expects the various possible inputs it can handle and which raises an error in all other cases:
def class_retaining_map(fun, iterable):
if type(iterable) is list: # not using isinstance(), see below for reasoning
return [ fun(x) for x in iterable ]
elif type(iterable) is set:
return { fun(x) for x in iterable }
elif type(iterable) is dict:
return { k: fun(v) for k, v in iterable.items() }
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return { fun(k): v for k, v in iterable.items() }
else:
raise TypeError("type %r not supported" % type(iterable))
You could add a case for all other iterable values in the else clause of cause:
else:
return (fun(x) for x in iterable)
But that would e. g. return an iterable for a subclass of set which might not be what you want.
Mind that I'm deliberately not using isinstance because that would make a list out of a subclass of list, for instance. I figure that this is explicitly not wanted in this case.
One could argue that anything which is a list (i. e. is a subclass of list) needs to comply to have a constructor which returns a thing of this type for an iteration of elements. And likewise for subclasses of set, dict (which must work for an iteration of pairs), etc. Then the code might look like this:
def class_retaining_map(fun, iterable):
if isinstance(iterable, (list, set)):
return type(iterable)(fun(x) for x in iterable)
elif isinstance(iterable, dict):
return type(iterable)((k, fun(v)) for k, v in iterable.items())
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return type(iterable)((fun(k), v) for k, v in iterable.items())
else:
raise TypeError("type %r not supported" % type(iterable))
Using the type of the input as a converter is not necessarily working in all occasions. map is just using the "iterability" of its input to produce its output. In Python3 this is why map returns a generator instead of a list (which is just more fitting).
So a cleaner and more robust version would be one which explicitly expects the various possible inputs it can handle and which raises an error in all other cases:
def class_retaining_map(fun, iterable):
if type(iterable) is list: # not using isinstance(), see below for reasoning
return [ fun(x) for x in iterable ]
elif type(iterable) is set:
return { fun(x) for x in iterable }
elif type(iterable) is dict:
return { k: fun(v) for k, v in iterable.items() }
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return { fun(k): v for k, v in iterable.items() }
else:
raise TypeError("type %r not supported" % type(iterable))
You could add a case for all other iterable values in the else clause of cause:
else:
return (fun(x) for x in iterable)
But that would e. g. return an iterable for a subclass of set which might not be what you want.
Mind that I'm deliberately not using isinstance because that would make a list out of a subclass of list, for instance. I figure that this is explicitly not wanted in this case.
One could argue that anything which is a list (i. e. is a subclass of list) needs to comply to have a constructor which returns a thing of this type for an iteration of elements. And likewise for subclasses of set, dict (which must work for an iteration of pairs), etc. Then the code might look like this:
def class_retaining_map(fun, iterable):
if isinstance(iterable, (list, set)):
return type(iterable)(fun(x) for x in iterable)
elif isinstance(iterable, dict):
return type(iterable)((k, fun(v)) for k, v in iterable.items())
# ^^^ use .iteritems() in python2!
# and depending on your usecase this might be more fitting:
# return type(iterable)((fun(k), v) for k, v in iterable.items())
else:
raise TypeError("type %r not supported" % type(iterable))
edited Jan 2 at 11:09
answered Jan 2 at 10:34
AlfeAlfe
32.9k1063108
32.9k1063108
1
Are you deliberately usingtype(iterable) == listinstead ofisinstance? I can think of very few cases were this is not a bad practice. And even then, you would useisinstead of==.
– Eli Korvigo
Jan 2 at 10:40
@EliKorvigo Yes, I'm usingtype(iterable)deliberately (see my last paragraph which I added late for the reason of this). I changed the use of==to the use ofiswhich is slightly better in this case, you are right.
– Alfe
Jan 2 at 10:43
1
I'll adopt this solution, with another case :elif type(iterable) is str: return "".join(map(fun, iterable))
– HNoob
Jan 2 at 10:55
1
In case you need strings, you might want to consider to addbytesin Python3 orunicodein Python2.
– Alfe
Jan 2 at 10:58
add a comment |
1
Are you deliberately usingtype(iterable) == listinstead ofisinstance? I can think of very few cases were this is not a bad practice. And even then, you would useisinstead of==.
– Eli Korvigo
Jan 2 at 10:40
@EliKorvigo Yes, I'm usingtype(iterable)deliberately (see my last paragraph which I added late for the reason of this). I changed the use of==to the use ofiswhich is slightly better in this case, you are right.
– Alfe
Jan 2 at 10:43
1
I'll adopt this solution, with another case :elif type(iterable) is str: return "".join(map(fun, iterable))
– HNoob
Jan 2 at 10:55
1
In case you need strings, you might want to consider to addbytesin Python3 orunicodein Python2.
– Alfe
Jan 2 at 10:58
1
1
Are you deliberately using
type(iterable) == list instead of isinstance? I can think of very few cases were this is not a bad practice. And even then, you would use is instead of ==.– Eli Korvigo
Jan 2 at 10:40
Are you deliberately using
type(iterable) == list instead of isinstance? I can think of very few cases were this is not a bad practice. And even then, you would use is instead of ==.– Eli Korvigo
Jan 2 at 10:40
@EliKorvigo Yes, I'm using
type(iterable) deliberately (see my last paragraph which I added late for the reason of this). I changed the use of == to the use of is which is slightly better in this case, you are right.– Alfe
Jan 2 at 10:43
@EliKorvigo Yes, I'm using
type(iterable) deliberately (see my last paragraph which I added late for the reason of this). I changed the use of == to the use of is which is slightly better in this case, you are right.– Alfe
Jan 2 at 10:43
1
1
I'll adopt this solution, with another case :
elif type(iterable) is str: return "".join(map(fun, iterable))– HNoob
Jan 2 at 10:55
I'll adopt this solution, with another case :
elif type(iterable) is str: return "".join(map(fun, iterable))– HNoob
Jan 2 at 10:55
1
1
In case you need strings, you might want to consider to add
bytes in Python3 or unicode in Python2.– Alfe
Jan 2 at 10:58
In case you need strings, you might want to consider to add
bytes in Python3 or unicode in Python2.– Alfe
Jan 2 at 10:58
add a comment |
Instantiate directly rather than via eval
__class__ can also be used to instantiate new instances:
def mymap(f, contener):
t = contener.__class__
return t(map(f, contener))
This removes the need for eval, use of which is considered poor practice. As per @EliKorvigo's comment, you may prefer built-in type to a magic method:
def mymap(f, contener):
t = type(contener)
return t(map(f, contener))
As explained here and in the docs:
The return value is a type object and generally the same object as returned by
object.__class__.
"Generally the same" should be considered "equivalent" in the case of new-style classes.
Testing for an iterable
You can check/test for an iterable in a couple of ways. Either use try / except to catch TypeError:
def mymap(f, contener):
try:
mapper = map(f, contener)
except TypeError:
return 'Input object is not iterable'
return type(contener)(mapper)
Or use collections.Iterable:
from collections import Iterable
def mymap(f, contener):
if isinstance(contener, Iterable):
return type(contener)(map(f, contener))
return 'Input object is not iterable'
This works specifically because built-in classes commonly used as containers such as list, set, tuple, collections.deque, etc, can be used to instantiate instances via a lazy iterable. Exceptions exist: for example, str(map(str.upper, 'hello')) will not work as you might expect even though str instances are iterable.
2
I'd say, callingtype(container)(map(...))is a bit cleaner than accessing a magic attribute.
– Eli Korvigo
Jan 2 at 10:22
1
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})
– Patrick Artner
Jan 2 at 10:33
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
1
Yourtry/exceptcode catches way too many cases. EachTypeErrorthrown in the execution offwithin themapcall will be caught and misinterpreted as "Input object is not iterable".
– Alfe
Jan 2 at 10:51
|
show 3 more comments
Instantiate directly rather than via eval
__class__ can also be used to instantiate new instances:
def mymap(f, contener):
t = contener.__class__
return t(map(f, contener))
This removes the need for eval, use of which is considered poor practice. As per @EliKorvigo's comment, you may prefer built-in type to a magic method:
def mymap(f, contener):
t = type(contener)
return t(map(f, contener))
As explained here and in the docs:
The return value is a type object and generally the same object as returned by
object.__class__.
"Generally the same" should be considered "equivalent" in the case of new-style classes.
Testing for an iterable
You can check/test for an iterable in a couple of ways. Either use try / except to catch TypeError:
def mymap(f, contener):
try:
mapper = map(f, contener)
except TypeError:
return 'Input object is not iterable'
return type(contener)(mapper)
Or use collections.Iterable:
from collections import Iterable
def mymap(f, contener):
if isinstance(contener, Iterable):
return type(contener)(map(f, contener))
return 'Input object is not iterable'
This works specifically because built-in classes commonly used as containers such as list, set, tuple, collections.deque, etc, can be used to instantiate instances via a lazy iterable. Exceptions exist: for example, str(map(str.upper, 'hello')) will not work as you might expect even though str instances are iterable.
2
I'd say, callingtype(container)(map(...))is a bit cleaner than accessing a magic attribute.
– Eli Korvigo
Jan 2 at 10:22
1
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})
– Patrick Artner
Jan 2 at 10:33
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
1
Yourtry/exceptcode catches way too many cases. EachTypeErrorthrown in the execution offwithin themapcall will be caught and misinterpreted as "Input object is not iterable".
– Alfe
Jan 2 at 10:51
|
show 3 more comments
Instantiate directly rather than via eval
__class__ can also be used to instantiate new instances:
def mymap(f, contener):
t = contener.__class__
return t(map(f, contener))
This removes the need for eval, use of which is considered poor practice. As per @EliKorvigo's comment, you may prefer built-in type to a magic method:
def mymap(f, contener):
t = type(contener)
return t(map(f, contener))
As explained here and in the docs:
The return value is a type object and generally the same object as returned by
object.__class__.
"Generally the same" should be considered "equivalent" in the case of new-style classes.
Testing for an iterable
You can check/test for an iterable in a couple of ways. Either use try / except to catch TypeError:
def mymap(f, contener):
try:
mapper = map(f, contener)
except TypeError:
return 'Input object is not iterable'
return type(contener)(mapper)
Or use collections.Iterable:
from collections import Iterable
def mymap(f, contener):
if isinstance(contener, Iterable):
return type(contener)(map(f, contener))
return 'Input object is not iterable'
This works specifically because built-in classes commonly used as containers such as list, set, tuple, collections.deque, etc, can be used to instantiate instances via a lazy iterable. Exceptions exist: for example, str(map(str.upper, 'hello')) will not work as you might expect even though str instances are iterable.
Instantiate directly rather than via eval
__class__ can also be used to instantiate new instances:
def mymap(f, contener):
t = contener.__class__
return t(map(f, contener))
This removes the need for eval, use of which is considered poor practice. As per @EliKorvigo's comment, you may prefer built-in type to a magic method:
def mymap(f, contener):
t = type(contener)
return t(map(f, contener))
As explained here and in the docs:
The return value is a type object and generally the same object as returned by
object.__class__.
"Generally the same" should be considered "equivalent" in the case of new-style classes.
Testing for an iterable
You can check/test for an iterable in a couple of ways. Either use try / except to catch TypeError:
def mymap(f, contener):
try:
mapper = map(f, contener)
except TypeError:
return 'Input object is not iterable'
return type(contener)(mapper)
Or use collections.Iterable:
from collections import Iterable
def mymap(f, contener):
if isinstance(contener, Iterable):
return type(contener)(map(f, contener))
return 'Input object is not iterable'
This works specifically because built-in classes commonly used as containers such as list, set, tuple, collections.deque, etc, can be used to instantiate instances via a lazy iterable. Exceptions exist: for example, str(map(str.upper, 'hello')) will not work as you might expect even though str instances are iterable.
edited Jan 2 at 10:57
answered Jan 2 at 10:16
jppjpp
102k2164115
102k2164115
2
I'd say, callingtype(container)(map(...))is a bit cleaner than accessing a magic attribute.
– Eli Korvigo
Jan 2 at 10:22
1
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})
– Patrick Artner
Jan 2 at 10:33
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
1
Yourtry/exceptcode catches way too many cases. EachTypeErrorthrown in the execution offwithin themapcall will be caught and misinterpreted as "Input object is not iterable".
– Alfe
Jan 2 at 10:51
|
show 3 more comments
2
I'd say, callingtype(container)(map(...))is a bit cleaner than accessing a magic attribute.
– Eli Korvigo
Jan 2 at 10:22
1
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})
– Patrick Artner
Jan 2 at 10:33
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
1
Yourtry/exceptcode catches way too many cases. EachTypeErrorthrown in the execution offwithin themapcall will be caught and misinterpreted as "Input object is not iterable".
– Alfe
Jan 2 at 10:51
2
2
I'd say, calling
type(container)(map(...)) is a bit cleaner than accessing a magic attribute.– Eli Korvigo
Jan 2 at 10:22
I'd say, calling
type(container)(map(...)) is a bit cleaner than accessing a magic attribute.– Eli Korvigo
Jan 2 at 10:22
1
1
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
@PatrickArtner, It certainly works for me.
– jpp
Jan 2 at 10:29
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?
mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})– Patrick Artner
Jan 2 at 10:33
Yepp works. Would it be wise to put a try:except: around it in case the function transforms the iterabletype to something that does not work? or is it better to let it crash if you call it with a typechanging function?
mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})– Patrick Artner
Jan 2 at 10:33
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
@PatrickArtner, Fair point, I added a couple of ways this can be done.
– jpp
Jan 2 at 10:38
1
1
Your
try/except code catches way too many cases. Each TypeError thrown in the execution of f within the map call will be caught and misinterpreted as "Input object is not iterable".– Alfe
Jan 2 at 10:51
Your
try/except code catches way too many cases. Each TypeError thrown in the execution of f within the map call will be caught and misinterpreted as "Input object is not iterable".– Alfe
Jan 2 at 10:51
|
show 3 more comments
I search a kind of "fmap" in haskell, but in python3
First, let's discuss Haskell's fmap to understand, why it behaves the way it does, though I assume you are fairly familiar with Haskell considering the question. fmap is a generic method defined in the Functor type-class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
...
Functors obey several important mathematical laws and have several methods derived from fmap, though the latter is sufficient for a minimal complete functor instance. In other words, in Haskell types belonging to the Functor type-class implement their own fmap functions (moreover, Haskell types can have multiple Functor implementations via newtype definitions). In Python we don't have type-classes, though we do have classes that, while less convenient in this case, allow us to simulate this behaviour. Unfortunately, with classes we can't add functionality to an already defined class without subclassing, which limits our ability to implement a generic fmap for all builtin types, though we can overcome it by explicitly checking for acceptable iterable types in our fmap implementation. It's also literally impossible to express higher-kinded types using Python's type system, but I digress.
To summarise, we've got several options:
- Support all
Iterabletypes (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smapback into the original type. That is the duty of applying a function over the values inside a container is taken away from the container. This approach differs drastically from the functor interface: functors are supposed to handle the mapping themselves and handle additional metadata crucial to reconstruct the container. - Support a subset of readily mappable builtin iterable types (i.e. builtins that don't carry any important metadata). This solution is implemented by @Alfe, and, while less generic, it is safer.
- Take solution #2 and add support for proper user-defined functors.
This is my take on the third solution
import abc
from typing import Generic, TypeVar, Callable, Union,
Dict, List, Tuple, Set, Text
A = TypeVar('A')
B = TypeVar('B')
class Functor(Generic[A], metaclass=abc.ABCMeta):
@abc.abstractmethod
def fmap(self, f: Callable[[A], B]) -> 'Functor[B]':
raise NotImplemented
FMappable = Union[Functor, List, Tuple, Set, Dict, Text]
def fmap(f: Callable[[A], B], fmappable: FMappable) -> FMappable:
if isinstance(fmappable, Functor):
return fmappable.fmap(f)
if isinstance(fmappable, (List, Tuple, Set, Text)):
return type(fmappable)(map(f, fmappable))
if isinstance(fmappable, Dict):
return type(fmappable)(
(key, f(value)) for key, value in fmappable.items()
)
raise TypeError('argument fmappable is not an instance of FMappable')
Here is a demo
In [20]: import pandas as pd
In [21]: class FSeries(pd.Series, Functor):
...:
...: def fmap(self, f):
...: return self.apply(f).astype(self.dtype)
...:
In [22]: fmap(lambda x: x * 2, [1, 2, 3])
Out[22]: [2, 4, 6]
In [23]: fmap(lambda x: x * 2, {'one': 1, 'two': 2, 'three': 3})
Out[23]: {'one': 2, 'two': 4, 'three': 6}
In [24]: fmap(lambda x: x * 2, FSeries([1, 2, 3], index=['one', 'two', 'three']))
Out[24]:
one 2
two 4
three 6
dtype: int64
In [25]: fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-27-1c4524f8e4b1> in <module>
----> 1 fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
<ipython-input-7-53b2d5fda1bf> in fmap(f, fmappable)
34 if isinstance(fmappable, Functor):
35 return fmappable.fmap(f)
---> 36 raise TypeError('argument fmappable is not an instance of FMappable')
37
38
TypeError: argument fmappable is not an instance of FMappable
This solution allows us to define multiple functors for the same type via subclassing:
In [26]: class FDict(dict, Functor):
...:
...: def fmap(self, f):
...: return {f(key): value for key, value in self.items()}
...:
...:
In [27]: fmap(lambda x: x * 2, FDict({'one': 1, 'two': 2, 'three': 3}))
Out[27]: {'oneone': 1, 'twotwo': 2, 'threethree': 3}
add a comment |
I search a kind of "fmap" in haskell, but in python3
First, let's discuss Haskell's fmap to understand, why it behaves the way it does, though I assume you are fairly familiar with Haskell considering the question. fmap is a generic method defined in the Functor type-class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
...
Functors obey several important mathematical laws and have several methods derived from fmap, though the latter is sufficient for a minimal complete functor instance. In other words, in Haskell types belonging to the Functor type-class implement their own fmap functions (moreover, Haskell types can have multiple Functor implementations via newtype definitions). In Python we don't have type-classes, though we do have classes that, while less convenient in this case, allow us to simulate this behaviour. Unfortunately, with classes we can't add functionality to an already defined class without subclassing, which limits our ability to implement a generic fmap for all builtin types, though we can overcome it by explicitly checking for acceptable iterable types in our fmap implementation. It's also literally impossible to express higher-kinded types using Python's type system, but I digress.
To summarise, we've got several options:
- Support all
Iterabletypes (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smapback into the original type. That is the duty of applying a function over the values inside a container is taken away from the container. This approach differs drastically from the functor interface: functors are supposed to handle the mapping themselves and handle additional metadata crucial to reconstruct the container. - Support a subset of readily mappable builtin iterable types (i.e. builtins that don't carry any important metadata). This solution is implemented by @Alfe, and, while less generic, it is safer.
- Take solution #2 and add support for proper user-defined functors.
This is my take on the third solution
import abc
from typing import Generic, TypeVar, Callable, Union,
Dict, List, Tuple, Set, Text
A = TypeVar('A')
B = TypeVar('B')
class Functor(Generic[A], metaclass=abc.ABCMeta):
@abc.abstractmethod
def fmap(self, f: Callable[[A], B]) -> 'Functor[B]':
raise NotImplemented
FMappable = Union[Functor, List, Tuple, Set, Dict, Text]
def fmap(f: Callable[[A], B], fmappable: FMappable) -> FMappable:
if isinstance(fmappable, Functor):
return fmappable.fmap(f)
if isinstance(fmappable, (List, Tuple, Set, Text)):
return type(fmappable)(map(f, fmappable))
if isinstance(fmappable, Dict):
return type(fmappable)(
(key, f(value)) for key, value in fmappable.items()
)
raise TypeError('argument fmappable is not an instance of FMappable')
Here is a demo
In [20]: import pandas as pd
In [21]: class FSeries(pd.Series, Functor):
...:
...: def fmap(self, f):
...: return self.apply(f).astype(self.dtype)
...:
In [22]: fmap(lambda x: x * 2, [1, 2, 3])
Out[22]: [2, 4, 6]
In [23]: fmap(lambda x: x * 2, {'one': 1, 'two': 2, 'three': 3})
Out[23]: {'one': 2, 'two': 4, 'three': 6}
In [24]: fmap(lambda x: x * 2, FSeries([1, 2, 3], index=['one', 'two', 'three']))
Out[24]:
one 2
two 4
three 6
dtype: int64
In [25]: fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-27-1c4524f8e4b1> in <module>
----> 1 fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
<ipython-input-7-53b2d5fda1bf> in fmap(f, fmappable)
34 if isinstance(fmappable, Functor):
35 return fmappable.fmap(f)
---> 36 raise TypeError('argument fmappable is not an instance of FMappable')
37
38
TypeError: argument fmappable is not an instance of FMappable
This solution allows us to define multiple functors for the same type via subclassing:
In [26]: class FDict(dict, Functor):
...:
...: def fmap(self, f):
...: return {f(key): value for key, value in self.items()}
...:
...:
In [27]: fmap(lambda x: x * 2, FDict({'one': 1, 'two': 2, 'three': 3}))
Out[27]: {'oneone': 1, 'twotwo': 2, 'threethree': 3}
add a comment |
I search a kind of "fmap" in haskell, but in python3
First, let's discuss Haskell's fmap to understand, why it behaves the way it does, though I assume you are fairly familiar with Haskell considering the question. fmap is a generic method defined in the Functor type-class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
...
Functors obey several important mathematical laws and have several methods derived from fmap, though the latter is sufficient for a minimal complete functor instance. In other words, in Haskell types belonging to the Functor type-class implement their own fmap functions (moreover, Haskell types can have multiple Functor implementations via newtype definitions). In Python we don't have type-classes, though we do have classes that, while less convenient in this case, allow us to simulate this behaviour. Unfortunately, with classes we can't add functionality to an already defined class without subclassing, which limits our ability to implement a generic fmap for all builtin types, though we can overcome it by explicitly checking for acceptable iterable types in our fmap implementation. It's also literally impossible to express higher-kinded types using Python's type system, but I digress.
To summarise, we've got several options:
- Support all
Iterabletypes (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smapback into the original type. That is the duty of applying a function over the values inside a container is taken away from the container. This approach differs drastically from the functor interface: functors are supposed to handle the mapping themselves and handle additional metadata crucial to reconstruct the container. - Support a subset of readily mappable builtin iterable types (i.e. builtins that don't carry any important metadata). This solution is implemented by @Alfe, and, while less generic, it is safer.
- Take solution #2 and add support for proper user-defined functors.
This is my take on the third solution
import abc
from typing import Generic, TypeVar, Callable, Union,
Dict, List, Tuple, Set, Text
A = TypeVar('A')
B = TypeVar('B')
class Functor(Generic[A], metaclass=abc.ABCMeta):
@abc.abstractmethod
def fmap(self, f: Callable[[A], B]) -> 'Functor[B]':
raise NotImplemented
FMappable = Union[Functor, List, Tuple, Set, Dict, Text]
def fmap(f: Callable[[A], B], fmappable: FMappable) -> FMappable:
if isinstance(fmappable, Functor):
return fmappable.fmap(f)
if isinstance(fmappable, (List, Tuple, Set, Text)):
return type(fmappable)(map(f, fmappable))
if isinstance(fmappable, Dict):
return type(fmappable)(
(key, f(value)) for key, value in fmappable.items()
)
raise TypeError('argument fmappable is not an instance of FMappable')
Here is a demo
In [20]: import pandas as pd
In [21]: class FSeries(pd.Series, Functor):
...:
...: def fmap(self, f):
...: return self.apply(f).astype(self.dtype)
...:
In [22]: fmap(lambda x: x * 2, [1, 2, 3])
Out[22]: [2, 4, 6]
In [23]: fmap(lambda x: x * 2, {'one': 1, 'two': 2, 'three': 3})
Out[23]: {'one': 2, 'two': 4, 'three': 6}
In [24]: fmap(lambda x: x * 2, FSeries([1, 2, 3], index=['one', 'two', 'three']))
Out[24]:
one 2
two 4
three 6
dtype: int64
In [25]: fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-27-1c4524f8e4b1> in <module>
----> 1 fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
<ipython-input-7-53b2d5fda1bf> in fmap(f, fmappable)
34 if isinstance(fmappable, Functor):
35 return fmappable.fmap(f)
---> 36 raise TypeError('argument fmappable is not an instance of FMappable')
37
38
TypeError: argument fmappable is not an instance of FMappable
This solution allows us to define multiple functors for the same type via subclassing:
In [26]: class FDict(dict, Functor):
...:
...: def fmap(self, f):
...: return {f(key): value for key, value in self.items()}
...:
...:
In [27]: fmap(lambda x: x * 2, FDict({'one': 1, 'two': 2, 'three': 3}))
Out[27]: {'oneone': 1, 'twotwo': 2, 'threethree': 3}
I search a kind of "fmap" in haskell, but in python3
First, let's discuss Haskell's fmap to understand, why it behaves the way it does, though I assume you are fairly familiar with Haskell considering the question. fmap is a generic method defined in the Functor type-class:
class Functor f where
fmap :: (a -> b) -> f a -> f b
...
Functors obey several important mathematical laws and have several methods derived from fmap, though the latter is sufficient for a minimal complete functor instance. In other words, in Haskell types belonging to the Functor type-class implement their own fmap functions (moreover, Haskell types can have multiple Functor implementations via newtype definitions). In Python we don't have type-classes, though we do have classes that, while less convenient in this case, allow us to simulate this behaviour. Unfortunately, with classes we can't add functionality to an already defined class without subclassing, which limits our ability to implement a generic fmap for all builtin types, though we can overcome it by explicitly checking for acceptable iterable types in our fmap implementation. It's also literally impossible to express higher-kinded types using Python's type system, but I digress.
To summarise, we've got several options:
- Support all
Iterabletypes (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smapback into the original type. That is the duty of applying a function over the values inside a container is taken away from the container. This approach differs drastically from the functor interface: functors are supposed to handle the mapping themselves and handle additional metadata crucial to reconstruct the container. - Support a subset of readily mappable builtin iterable types (i.e. builtins that don't carry any important metadata). This solution is implemented by @Alfe, and, while less generic, it is safer.
- Take solution #2 and add support for proper user-defined functors.
This is my take on the third solution
import abc
from typing import Generic, TypeVar, Callable, Union,
Dict, List, Tuple, Set, Text
A = TypeVar('A')
B = TypeVar('B')
class Functor(Generic[A], metaclass=abc.ABCMeta):
@abc.abstractmethod
def fmap(self, f: Callable[[A], B]) -> 'Functor[B]':
raise NotImplemented
FMappable = Union[Functor, List, Tuple, Set, Dict, Text]
def fmap(f: Callable[[A], B], fmappable: FMappable) -> FMappable:
if isinstance(fmappable, Functor):
return fmappable.fmap(f)
if isinstance(fmappable, (List, Tuple, Set, Text)):
return type(fmappable)(map(f, fmappable))
if isinstance(fmappable, Dict):
return type(fmappable)(
(key, f(value)) for key, value in fmappable.items()
)
raise TypeError('argument fmappable is not an instance of FMappable')
Here is a demo
In [20]: import pandas as pd
In [21]: class FSeries(pd.Series, Functor):
...:
...: def fmap(self, f):
...: return self.apply(f).astype(self.dtype)
...:
In [22]: fmap(lambda x: x * 2, [1, 2, 3])
Out[22]: [2, 4, 6]
In [23]: fmap(lambda x: x * 2, {'one': 1, 'two': 2, 'three': 3})
Out[23]: {'one': 2, 'two': 4, 'three': 6}
In [24]: fmap(lambda x: x * 2, FSeries([1, 2, 3], index=['one', 'two', 'three']))
Out[24]:
one 2
two 4
three 6
dtype: int64
In [25]: fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-27-1c4524f8e4b1> in <module>
----> 1 fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))
<ipython-input-7-53b2d5fda1bf> in fmap(f, fmappable)
34 if isinstance(fmappable, Functor):
35 return fmappable.fmap(f)
---> 36 raise TypeError('argument fmappable is not an instance of FMappable')
37
38
TypeError: argument fmappable is not an instance of FMappable
This solution allows us to define multiple functors for the same type via subclassing:
In [26]: class FDict(dict, Functor):
...:
...: def fmap(self, f):
...: return {f(key): value for key, value in self.items()}
...:
...:
In [27]: fmap(lambda x: x * 2, FDict({'one': 1, 'two': 2, 'three': 3}))
Out[27]: {'oneone': 1, 'twotwo': 2, 'threethree': 3}
edited yesterday
answered Jan 2 at 13:31
Eli KorvigoEli Korvigo
6,17722853
6,17722853
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54004428%2fhow-can-i-write-a-function-fmap-that-returns-the-same-type-of-iterable-that-was%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
Breaks if:
fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}).. and in any other case where the function changes the type...– Patrick Artner
Jan 2 at 10:27