Maybe in a python (original title: Maybe in Python)

(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.

specification

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.

Basic

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

Maybe binding (cast with Null check)

To use Maybe [T] as T,

  1. Make sure that Maybe has a value,
  2. You must convert 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.

Forced unwrap operator (from !, !!)

Consider a function safe_int that attempts to convert from str to ʻintand returnsnothing` 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.

Null Conditional Operator (from ?., ?->)

Suppose you have a Maybe [list] here. You wanted to find out how many 0s 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.

Null fusion operator (from ?:, ??)

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.

  1. Some people write so that the handwritten “?” Extends downward from the “>” and makes a dot (similar in shape).
  2. On the keyboard, “?” And “>” are next to each other (similar to input operation)
  3. When writing multiple items side by side, it is easy to visually understand that the values are checked in order from the left (visualization of processing logic).
  4. Close to the priority of the Null fusion operator in other languages (less confusing)

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.

Other things

Maybe as a container

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 returnsTruewhen the contents of the right-hand side are equal to the left-hand side. Otherwise, it returnsFalse. 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.

Extreme Swift Optional type

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.

Connect with the native world

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}

Differences from PyMaybe and responses to “Isn't it just a hassle?”

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 checked nil, but sometimes it just happens to work. And one day, when certain conditions overlap or a small fix is made, the bug becomes apparent.

Stop Objective-C now and use Swift

Objective-C's handling of nils 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 the nil handling on the Objective-C side was improper only after a NullPointerException occurred in Java. was.

Null insecure languages are no longer legacy languages

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

Maybe in a python (original title: Maybe in Python)
Take a screenshot in Python
Create a function in Python
Create a dictionary in Python
Make a bookmarklet in Python
Draw a heart in Python
Write a binary search in Python
[python] Manage functions in a list
Hit a command in Python (Windows)
Create a DI Container in Python
Draw a scatterplot matrix in python
ABC166 in Python A ~ C problem
Write A * (A-star) algorithm in Python
Create a binary file in Python
Solve ABC036 A ~ C in Python
Write a pie chart in Python
Write a vim plugin in Python
Write a depth-first search in Python
Implementing a simple algorithm in Python 2
Create a Kubernetes Operator in Python
Implementation of original sorting in Python
Solve ABC037 A ~ C in Python
Run a simple algorithm in Python
Draw a CNN diagram in Python
Create a random string in Python
Schedule a Zoom meeting in Python
When writing a program in Python
Generate a first class collection in Python
Spiral book in Python! Python with a spiral book! (Chapter 14 ~)
Solve ABC175 A, B, C in Python
Use print in a Python2 lambda expression
A simple HTTP client implemented in Python
Do a non-recursive Euler Tour in Python
I made a payroll program in Python!
Precautions when pickling a function in python
Write the test in a python docstring
Display a list of alphabets in Python 3
Try sending a SYN packet in Python
Try drawing a simple animation in Python
Create a simple GUI app in Python
Create a JSON object mapper in Python
Write a short property definition in Python
Draw a heart in Python Part 2 (SymPy)
[Python] [Windows] Take a screen capture in Python
Run the Python interpreter in a script
How to get a stacktrace in python
Write a Caesar cipher program in Python
Hash in Perl is a dictionary in Python
Scraping a website using JavaScript in Python
Write a simple greedy algorithm in Python
Launch a Flask app in Python Anywhere
Get a token for conoha in python
Solve ABC165 A, B, D in Python
[GPS] Create a kml file in Python
Write a simple Vim Plugin in Python 3
Draw a tree in Python 3 using graphviz
Generate a class from a string in Python
Display pyopengl in a browser (python + eel)
Let's make a combination calculation in Python
Try a functional programming pipe in Python
Learn dynamic programming in Python (A ~ E)