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) == list
instead ofisinstance
? I can think of very few cases were this is not a bad practice. And even then, you would useis
instead 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 ofis
which 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 addbytes
in Python3 orunicode
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
/except
code catches way too many cases. EachTypeError
thrown in the execution off
within themap
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
Iterable
types (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smap
back 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) == list
instead ofisinstance
? I can think of very few cases were this is not a bad practice. And even then, you would useis
instead 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 ofis
which 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 addbytes
in Python3 orunicode
in 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) == list
instead ofisinstance
? I can think of very few cases were this is not a bad practice. And even then, you would useis
instead 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 ofis
which 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 addbytes
in Python3 orunicode
in 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
33k1065110
33k1065110
1
Are you deliberately usingtype(iterable) == list
instead ofisinstance
? I can think of very few cases were this is not a bad practice. And even then, you would useis
instead 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 ofis
which 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 addbytes
in Python3 orunicode
in Python2.
– Alfe
Jan 2 at 10:58
add a comment |
1
Are you deliberately usingtype(iterable) == list
instead ofisinstance
? I can think of very few cases were this is not a bad practice. And even then, you would useis
instead 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 ofis
which 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 addbytes
in Python3 orunicode
in 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
/except
code catches way too many cases. EachTypeError
thrown in the execution off
within themap
call 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
/except
code catches way too many cases. EachTypeError
thrown in the execution off
within themap
call 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
102k2165115
102k2165115
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
/except
code catches way too many cases. EachTypeError
thrown in the execution off
within themap
call 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
/except
code catches way too many cases. EachTypeError
thrown in the execution off
within themap
call 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
Iterable
types (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smap
back 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
Iterable
types (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smap
back 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
Iterable
types (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smap
back 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
Iterable
types (@jpp's solution). It relies on constructors to convert an iterator returned by Python'smap
back 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 Mar 12 at 15:10
answered Jan 2 at 13:31
Eli KorvigoEli Korvigo
6,18623054
6,18623054
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