(It's not a translated article) No.
Maybe is good, isn't it? It makes it very easy to handle the state that there is a value and the state that there is no value. It is the height of the culture created by Lilin.
So, I thought I could do something similar with Python and implemented it.
The main body is listed here [https://gist.github.com/BlueRayi/fb999947e8f2a8037e7eea7ff9320f90). I think you should definitely play and throw Masakari.
Some of them are explicitly conscious of "null insecure languages are no longer legacy languages", so please read them together. I think it's good.
This Maybe is implemented as concrete classes Something
and Nothing
that inherit from the abstract class Maybe
. Something
indicates that there is a value, and Nothing
indicates that the value is missing.
The just
function creates an instance of Something
. nothing
is the (only) instance of Nothing
.
from maybe import just, nothing
a = just(42)
b = nothing
print(a)
print(b)
Just 42
Nothing
Maybe has the property has_value
, Something
returns True
and Nothing
returns False
. Also, Something
is itself Truthy and Nothing
is itself Falsey.
print(a.has_value)
print(b.has_value)
print('-----')
if a:
print('a is Truthy')
else:
print('a is Falsy')
if b:
print('b is Truthy')
else:
print('b is Falsy')
True
False
-----
a is Truthy
b is Falsy
Maybe can be thought of as a box to put values in. Maybe itself can be a value, so Maybe can be nested. Maybe has the concept of “depth”. The depth of the bare value is 0. The depth of Nothing
is 1. The depth of Something
is“ depth of contents + 1 ”. The depth can be obtained by passing it to the dep
function.
from maybe import dep
a2 = just(a)
b2 = just(b)
print(a2)
print(b2)
print('-----')
print(dep(42))
print(dep(a))
print(dep(b))
print(dep(a2))
print(dep(b2))
Just Just 42
Just Nothing
-----
0
1
1
2
2
Maybe can compare equal values. Something
is assumed to be equal if they are Something
and their contents are equal. If it is a nested structure, it will be dug while recurring. Nothing
is assumed to be equal if they are Nothing
.
print(a == 42)
print(a == just(42))
print(a == a2)
print(a == b)
print(b == nothing)
print(b == b2)
print(b2 == just(nothing))
False
True
False
False
True
False
True
To use Maybe [T]
as T
,
Maybe [T]
to T
.It is possible to use it as follows by using the if statement and the forced unwrap operator described later.
m = just(913)
if m.has_value:
print(m.i + 555)
1488
However, if these two processes are separated, the following unexpected happenings can occur.
m = just(913)
if m.has_value:
print(m + 555) #Forget unwrap, Maybe[int]And int cannot be added
TypeError: unsupported operand type(s) for +: 'Something' and 'int'
m = nothing
print(m.i + 555) #Forget Null check, if m is Nothing, the Nullpo will fly
maybe.NullpoError: it GOT no value.
In fact, Maybe can be iterated, Something
will only generate the value of the contents once, and Nothing
will generate a StopIteration ()
exception without generating anything. By using this, you can use the for statement to perform Null check and cast at the same time as shown below.
m = just(913)
for v in m:
print(v + 555) #This is only executed when m is Something
1468
I call this the “Maybe binding” because it feels just like the Swift Optional binding. The difference from the Optional binding is that shadowing (using the same variable name in the block) is not possible and the branching between missing values is a bit complicated.
Let's look at them in order. If you do something like shadowing, due to Python's variable scope, m
will change fromMaybe [int]
to ʻint` in the comment line.
m = just(913)
print(m) #Here Maybe[int]
for m in m:
print(m + 555)
print(m) #Here int
Just 913
1468
913
Do you find this more convenient because you've already checked Null? But it's a trap. If the value of m
is missing, it remainsMaybe [int]
. You are not allowed to deal with m
in the same context of“ Maybe ”from now on, you have to spend your time scared of the possibility that m
, which looks like ʻint, is
nothing. I don't know what Maybe is for. Isn't it not much different from just using
None` (it's even more annoying)?
The next difference. You may want to branch the process with and without a value, rather than just doing it when there is a value. In Swift Optional, you can write as follows.
let m: Int? = nil
if let v = m {
print(v + 555)
} else {
print("Maybe you hate me?")
}
The correspondence between ʻif and ʻelse
is beautiful and unrivaled. And these two blocks are grammatically paired.
However, in Maybe, we have to write like this.
m = nothing
for v in m:
print(v + 555)
if not m.has_value:
print("Maybe you hate me?")
The correspondence between for
and ʻif was probably born and no one has ever seen it. Besides, these two blocks are written next to each other and are not a grammatical pair. Furthermore, ʻif not m.has_value
is quite long. Regarding this, Nothing
is originally Falsey, so ʻif not m` is fine, but there is a drawback that the meaning of the sentence is a little difficult to read.
That said, it's much safer and easier to write than the Null check and cast are separated. Basically, it is better to prohibit the use of .i
and use the Maybe binding.
!
, !!
)Consider a function safe_int
that attempts to convert from str
to ʻintand returns
nothing` if it fails.
def safe_int(s):
try:
return just(int(s))
except ValueError:
return nothing
By the way, let's say that this function contains a string that jumps from the "age" text box used on a certain site. Normally, it will be restricted so that only numbers can be entered on the HTML side, and preprocessing etc. will be performed with JavaScript in the previous stage.
In such cases, it is virtually impossible for this function to return nothing
. Still, Maybe [int]
cannot be used as ʻint, so do I have to write a Maybe binding for a long time? Then it is better to use
None`.
That's when you should use the forced unwrap operator .i
. By the way, although it is called an operator, it is actually a property.
s = get_textbox('age')
age = safe_int(s).i # .i assigns an integer to age
if age >= 20:
...
It seems convenient and hassle-free, but if you call .i
for nothing
, the good old one will fly the original NullpoError
. Again, the forced unwrap operator is considered an “Unsafe” operation and should only be used where it can never be nothing
in logic.
?.
, ?->
)Suppose you have a Maybe [list]
here. You wanted to find out how many 0
s are in this. I think Maybe could be nothing
, but in most cases if the list itself doesn't exist, you'd still expect nothing
(no, some people want 0
). See also the following “Null Healing Operator”).
To be honest, it looks like this:
l = get_foobar_list('hogehoge')
for l0 in l:
num_0 = just(l0.count(0))
if not l:
num_0 = nothing
I can write it for sure, but to be clear, it's complicated. If l
is list
, you don't want to write the process that requires num_0 = l.count (0)
over 4 lines.
You can write it concisely using the Null conditional operator .q.
(as usual, it's really a property).
l = get_foobar_list('hogehoge')
num_0 = l.q.count(0)
You can also use .q []
for subscripts as well.
l = get_foobar_list('hogehoge')
print(l.q[3])
#Can be used instead of
# for l0 in l:
# print(just(l0[3]))
# if not l:
# print(nothing)
Why can I do this? .Q
returns an object that inherits from the abstract classSafeNavigationOperator
. SafeNavigationOperator
overrides __getattr__
and __getitem__
. If it has a value, Something SNO
is returned. This holds the value information, and __getattr__
and __getitem__
wrap it in just
and return it. If the value is missing, NothingSNO
is returned, and __getattr__
and __getitem__
just return nothing
.
You may be wondering if you have a good idea here. some_list.count
is a method and a callable object. maybe_list.q.count
is returned by further wrapping the function in Maybe. Is it a callable object of Maybe itself that it can be treated like l.q.count (0)
?
In fact, that's right. Calling Something
calls the contents and wraps the result in just
and returns it. Calling Nothing
simply returns nothing
. This specification makes it possible to do the things mentioned above. This specification is for use at the same time as .q.
. I personally think that it is magical to use it in other ways, so I think it is better to refrain from using it (it leads to the reason why the four arithmetic operations between Maybe, which will be described later, are not implemented).
By the way, even among the languages that implement ? .
, if the so-called" optional chain "is adopted, it will end when Null is found (so to speak, short-circuit evaluation), but .q.
In the case of, the process continues with nothing
. Even if you do .q.
for nothing
, you will only get nothing
, and the result of propagation is still the equivalent of Null, but you should be aware of this difference. You may need to keep it.
By the way, you can't do something like foo.q.bar = baz
because you haven't overridden __setattr__
. This is my technical limitation and I just couldn't solve the infinite loop that occurs when I override __setattr__
. Please help me ... I think that foo.q [bar] = baz
can be done, but it is not implemented so far because the symmetry is broken.
?:
, ??
)In the case of Null, wanting to fill in with the default value is probably the most common request when dealing with Null.
Of course you can also write with Maybe binding, but ...
l = get_foobar_list('hogehoge')
num_0 = l.q.count(0) #By the way, here is num_0 is Maybe[int]so……
for v in num_0:
num_0 = v
if not num_0:
num_0 = 0
#If you get out of here, num_0 is an int.
Properly Null fusion operator [^ coalesce] >>
is prepared.
l = get_foobar_list('hogehoge')
num_0 = l.q.count(0) >> 0 # num_0 is int
The behavior of the Null fusion operator looks simple at first glance (and is actually simple), but it requires a little attention.
When the value on the left side is missing, the story is easy and the right side is returned as it is.
On the other hand, when the left side has a value, it behaves interestingly (in the translational sense of Interesting). If the left side is deeper than the right side, the contents of the left side and the right side are reunited. If the right side is deeper than the left side, wrap the left side with just
and then heal again. If they are the same, the left side is returned. However, if the right side is a bare value (depth 0), wrap the right side with just
and heal again, and return the contents.
The ʻor` operator, which behaves similarly when viewed from the outside, is much more complicated than returning it as it is if the left side is Truthy. The reason for doing this is that the recursive call always returns a value with the same depth as the right-hand side.
In the context of “setting a default value”, you would expect it to be treated the same with or without a value. Maybe is nestable, which requires some cumbersome operation. However, that is an internal story, and the user can simply handle it as “always returning a value with the same structure”.
For example, when there are multiple Maybe, you can get “Maybe with the same structure as the rightmost side (bare value in this case), where the value appears earliest” as follows, regardless of their structure. I will.
x = foo >> bar >> baz >> qux >> 999
On the other hand, please note that the right side is evaluated because it is not a short-circuit evaluation.
By the way, this operator is implemented by overloading the right bit shift operator. Therefore, the order of precedence of operations is the same. That is, it is lower than the addition operator and higher than the comparison operator [^ operator-precedence]. This is close to the precedence of the Null fusion operator in C # [^ c-sharp], Swift [^ swift], Kotlin [^ kotlin], but on the other hand the None-aware operator
proposed in PEP 505. This is very different from [^ pep-505], where ?? has a higher priority than most binary operators. Please be careful when the day when PEP 505 is officially adopted (I don't think it will come). Also,
>> =` is automatically defined.
a = just(42)
b = nothing
print(a >> 0 + 12) # 42 ?? 0 +12 would be 54(Haz)
print('-----')
a >>= 999
b >>= 999
print(a)
print(b)
42
-----
42
999
As an aside, I chose >>
as the overload destination.
And so on. ~~ It seems that the lower you go, the more rational the reason, but the scary thing is that the lower you go, the more the reason is retrofitting, and you can see that the author made Maybe appropriately. ~~
[^ coalesce]: Generally, the translation of Null Coalescing Operator is “Null coalescing operator”, but “coalescence” lacks the nuance of “filling in the missing part”, so it means “close the wound”. "Healing" is used.
map
method (from map
)The Null conditional operator actually has its weaknesses. It can only be used with Maybe objects. You also cannot give SafeNavigationOperator
as an argument.
What that means is that you can't do this.
l = just([1, 2, 3, ])
s = .q.sum(l) #Grammar error
s = sum(l.q) #Exception to pop
#Why don't you do this
#Oah, oh, oh, oh, oh, ♥
You can write with Maybe binding, but it's still complicated.
l = just([1, 2, 3, ])
for l0 in l:
s = just(sum(l0))
if not l:
s = nothing
There is a map
method to do this from the Maybe object side. Using the map
method, you can write:
l = just([1, 2, 3, ])
s = l.map(sum)
The function passed to the map
method returns the result wrapped in just
. Of course, if the value is missing, nothing
will be returned. You can also use lambda expressions in your functions, so you can do the following:
a = just(42)
print(a.map(lambda x: x / 2))
Just 21.0
There's not much to say about the map
method (especially if you know monads).
bind
method (from flatMap
)Recall the safe_int
function we created earlier. It takes str
as an argument and returnsMaybe [int]
. Let's apply this to Maybe [str]
using map
.
s1 = just('12')
s2 = just('hogehoge')
s3 = nothing
print(s1.map(safe_int))
print(s2.map(safe_int))
print(s3.map(safe_int))
Just Just 12
Just Nothing
Nothing
safe_int
returns Maybe, and the map
method rewraps it with just
, so it's nested. Perhaps the person who wrote this program didn't want this ending (the fact that "can" be nested makes it more expressive. For example, if the result of JSON parsing contains null
, thenjust (nothing)
, because if the key didn't exist in the first place, it could be expressed as nothing
).
It's been kept secret until now, but you can use the join
function to crush nests by 1 step. It's a natural transformation μ.
from maybe import join
print(join(s1.map(safe_int)))
print(join(s2.map(safe_int)))
print(join(s3.map(safe_int)))
Just 12
Nothing
Nothing
And above all, it is desirable to combine “ map
and join
”. If you have a bind
method, you can use it.
print(s1.bind(safe_int))
print(s2.bind(safe_int))
print(s3.bind(safe_int))
Just 12
Nothing
Nothing
For those who master Haskell ~~ metamorphosis ~~, you can easily tell that the map
method is the<$>
operator and the bind
method is the >> =
operator. Remember map
for functions that return bare values and bind
for functions that return Maybe.
do
function (from do
notation)Of course, map
and bind
are called from one Maybe object, so it is a little difficult to use for functions that take 2 or more arguments.
lhs = just(6)
rhs = just(9)
ans = lhs.bind(lambda x: rhs.map(lambda y: x * y))
print(ans)
Just 54
By the way, the inside is map
because it is a function that returns a bare value with x * y
, and the outside is bind
because the return value of the map
method is Maybe. Do you remember?
You can easily write this using the do
function.
from maybe import just, nothing, do
lhs = just(6)
rhs = just(9)
ans = do(lhs, rhs)(lambda x, y: x * y)
print(ans)
Just 54
The do
function retrieves the contents and executes the function only when all the arguments have values. Returns nothing
if any value is missing. The name do
comes from Haskell, but the algorithm is extremely anti-monadic. Forgive me.
By the way, the argument of the do
function can only be taken by Maybe. If you want to use bare values as some arguments, wrap them in just
.
Maybe is also a container, and you can use the len
and ʻin` operators.
The len
function always returns 1
for Something
and 0
for Nothing
.
a = just(42)
b = nothing
a2 = just(a)
b2 = just(b)
print(len(a))
print(len(b))
print(len(a2))
print(len(b2))
1
0
1
1
The ʻinoperator returns
Truewhen the contents of the right-hand side are equal to the left-hand side. Otherwise, it returns
False.
Nothing always returns
False`.
print(42 in a)
print(42 in b)
print(42 in a2)
print(42 in b2)
True
False
False
False
As mentioned earlier, Maybe can be iterated. So, for example, you can convert it to a list. @koher said: That's exactly what the result is.
ʻOptional
was a box that might be empty. From another point of view, you can think of ʻOptional
as ʻArray`, which can contain at most one element.
al = list(a)
bl = list(b)
print(al)
print(bl)
[42]
[]
It is a mystery whether there is an opportunity to use the specifications around here.
Maybe has a pair of functions and methods, maybe_from
and to_native
. It is a pair of functions and methods. These work to connect the native types of Python with Maybe.
maybe_from
takes a native value and returns Maybe. The difference from just
is that when None
is given, nothing
is returned instead ofjust (None)
, and when Maybe is given, it is returned as is. This function can be used to collectively use the return values of existing functions and methods that indicate missing return values as None
in the common context of Maybe.
#An existing function that returns None instead of price when it is not in the dictionary
def get_price(d, name, num, tax_rate=0.1):
if name in d:
return -(-d[name] * num * (1 + tax_rate)) // 1
else:
return None
# ...
from maybe import maybe_from
p = get_price(unit_prices, 'apple', 5) #int may come, None may come
p = maybe_from(p) #p is unit_To prices'apple'Maybe with or without[int]
The to_native
method, on the other hand, makes Maybe a native value (although if it's native, it's just an object that's already provided by some library). Unlike .i
, nothing
returns None
(but don't abuse it as a" safe "unwrap operator), and even if it's nested, it digs recursively and is always naked. Returns the value of. map
considers the whole thing missing when the argument is missing, but it can be used as an argument to a function that expects to give None
, which means it has no value. You can also specify it as a JSON serialization method by using the fact that it returns a native value (although it is abbreviated as native).
import json
from maybe import Maybe
def json_serial(obj):
if isinstance(obj, Maybe):
return obj.to_native()
raise TypeError ("Type {type(obj)} not serializable".format(type(obj)))
item = {
'a': just(42),
'b': nothing
}
jsonstr = json.dumps(item, default=json_serial)
print(jsonstr)
{"a": 42, "b": null}
When you say Maybe in Python, there is an existing library called “PyMaybe”. It seems to be a famous place mentioned in PEP 505.
The big feature here is that you can handle Maybe as if it were a bare value. Specifically, the following code in README.rst may be helpful. Think of maybe
as the maybe_from
here, and ʻor_else as the
>> `operator here.
>>> maybe('VALUE').lower()
'value'
>>> maybe(None).invalid().method().or_else('unknwon')
'unknwon'
We are calling a method for bare values for values that are wrapped in Maybe. You can also see that the method call for Null is ignored and Null is propagated. You don't have to take it out or do .q.
like my Maybe. It seems that Dunder methods such as four arithmetic operations are also implemented.
Why isn't this more convenient? I think many people think that there will be no extra errors.
However, there is a reason why I implemented Maybe with such a specification. It's just “to raise an error”.
If you don't correctly recognize the difference between Maybe [T]
and T
, you will get an error immediately, which helps you to be aware of the difference. If you confuse it, you will get an error immediately, and if you convert it to a string, it will be prefixed with an extra Just
. It is to ensure that "if you don't write it correctly, it won't work". If you make a mistake, you'll immediately notice it (see: If you're serious, don't be afraid of'Optional (2018)'). This is also the reason why I don't want Maybe to be called directly (because it hides the difference, though I really want to erase the string conversion ...).
It's generally understood that Null safety is a mechanism that doesn't cause nullpo, and there's no doubt that it has the ultimate goal (and suddenly I started talking about Null safety, but my Maybe's type hints are useless, so it's virtually impossible to use them for Null safety). However, what is really needed for Null safety is “a distinction between Nullable and Non-null”. Why didn't Java fail? It was that you could assign null
to any reference type, and conversely, any reference type's value could be null
. The fact that T
may be Null means that you can't use it as T
even though you wrote T
. Should be. Getting there and further breaking the barrier between Nullable and Non-null — letting things that aren't T
be treated like T
— is rather dangerous.
Objective-C has a bad spec that method calls to
nil
are ignored without error.(Omitted)
But in exchange for this convenience, Objective-C also carries a terrifying risk. I forgot to check
nil
where I should have checkednil
, but sometimes it just happens to work. And one day, when certain conditions overlap or a small fix is made, the bug becomes apparent.
Objective-C's handling of
nil
s seems safe at first glance, only delaying the discovery of problems and making them more difficult to resolve. I think the Objective-C method is the worst in that it is prone to potential bugs. Once upon a time, when I ported a program written in Objective-C to Java, I often noticed that thenil
handling on the Objective-C side was improper only after aNullPointerException
occurred in Java. was.
Of course, that doesn't mean you have to be less productive for Null safety. Null check for Nullable variables was necessary “even in the old language” [^ null-check], and this Maybe rather implemented various ways to simplify it. If you want to hit it without checking Null, there is even an Unsafe operation for that. This area is [a story told a few years ago](https://qiita.com/koher/items/e4835bd429b88809ab33#null-%E5%AE%89%E5%85%A8%E3%81%AF%E7 % 94% 9F% E7% 94% A3% E6% 80% A7% E3% 82% 82% E9% AB% 98% E3% 82% 81% E3% 82% 8B).
However, I think that those who like to use Python are aware of such ruggedness and value "write what you want to write honestly" anyway. So, I can't say that PyMaybe's idea is wrong ~~ I can say that I am the Maybe [T] errorist
who rebels against the world ~~. However, please do not hesitate to say that you are half-playing, or that you have written what you want to write.
[^ null-check]: This is an unrealistic story for a personalized Maybe, but if [use only nothing
and not the built-in None
](https://twitter.com/ kmizu / status / 782360813340286977) If you can (wrap it in maybe_from
as soon as it can occur), you don't need to check any Null for bare values. In fact, this is the biggest advantage of Null-safe languages, and Null-safe languages that have such a mechanism built into the language specification only need to have a “necessary Null check”. The general perception that “Null safe languages force Null checks” is correct, but the opposite is true. Reference: [Anti-pattern] Syndrome that may be all null (null)
Recommended Posts