Fable
Fable copied to clipboard
Python DU refinements: provide access to DU case constructors to python code
I'd like to provide DU case constructors to the python code that interops with code generated by Fable.
This suggested PR seems to do what I want, albeit there is edge case of generic type annotation not being carried in the case constructor of union with generic type argument, it doesn't seem to cause an issue to the python interpreter (and I'm not clear those would be required either).
Short quick test:
type U = Z of value: int | Y of name: string
let u1 = Z 1
let u2 = Y "abc"
let a : U = Fable.Core.PyInterop.emitPyExpr 1 "U.Z($0)"
let b : U = Fable.Core.PyInterop.emitPyExpr "abc" "U.Y($0)"
Fable.Core.Testing.Assert.AreEqual(a, u1)
Fable.Core.Testing.Assert.AreEqual(b, u2)
Turned by Fable into:
from __future__ import annotations
from typing import (Any, List)
from fable_modules.fable_library.reflection import (TypeInfo, int32_type, string_type, union_type)
from fable_modules.fable_library.types import (Array, Union)
from fable_modules.fable_library.util import assert_equal
def _expr0() -> TypeInfo:
return union_type("QuickTest.U", [], U, lambda: [[("value", int32_type)], [("name", string_type)]])
class U(Union):
def __init__(self, tag: int, *fields: Any) -> None:
super().__init__()
self.tag: int = tag or 0
self.fields: Array[Any] = list(fields)
@staticmethod
def cases() -> List[str]:
return ["Z", "Y"]
@staticmethod
def Z(value: int) -> U:
return U(0, value)
@staticmethod
def Y(name: str) -> U:
return U(1, name)
U_reflection = _expr0
u1: U = U(0, 1)
u2: U = U(1, "abc")
a: U = U.Z(1)
b: U = U.Y("abc")
assert_equal(a, u1)
assert_equal(b, u2)
cc: @dbrattli
Note that I also intended to work on exposing Is* members, but https://github.com/dotnet/fsharp/pull/11394 has not been merged.
I'm wondering if I should still go forward, because there is still no easy way to "pattern match" / branch on the DU values from consuming python code (AFAIK).
Any opinion how those Is* members should look like in Python?
There is a bit of a conflict with the DU cases having the PascalCase "type" casing and this being an instance get property which would mandate snake_case.
Experimenting with how we could support pattern matching of DUs from Python:
class _U(Union):
def __init__(self, tag: int, *fields: Any) -> None:
super().__init__()
self.tag: int = tag or 0
self.fields: Array[Any] = list(fields)
@staticmethod
def cases() -> list[str]:
return ["Z", "Y"]
class Z(_U):
__match_args__ = ("value",)
def __init__(self, value: int) -> None:
self.value = value
super().__init__(0, value)
class Y(_U):
__match_args__ = ("name",)
def __init__(self, name: str) -> None:
self.name = name
super().__init__(1, name)
U = Z | Y
def create() -> U:
return Z(1)
u: U = create()
match u:
case Z(value=10):
print("Z")
case Y(name=name):
print("Y")
case _:
print("Not Z or Y")
@dbrattli this looks interesting!
Would it make more sense to put the cases as inner classes? I'm wondering because there is the concern of having DU cases that are named the same as outer type, which is possible in F#, but would likely cause some headaches on the python side.
type Case1 = { a:int }
type Case2 = { b:string }
type DU =
| Case1 of Case1
| Case2 of Case2
Btw some reading I found:
- https://github.com/Microsoft/pyright/issues/2160
- https://www.fullstory.com/blog/discriminated-unions-and-exhaustiveness-checking-in-typescript/
- https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
- https://mypy.readthedocs.io/en/stable/literal_types.html#tagged-unions
- https://github.com/fable-compiler/Fable/pull/2618
- https://fable.io/docs/typescript/features.html#tagged-unions
We should investigate if we should leave the DUs as is and instead make a feature similar to TypeScriptTaggedUnion for Python to generate DUs that are more Pythonic using literal types as discriminator such as with ts.