Fable icon indicating copy to clipboard operation
Fable copied to clipboard

Python DU refinements: provide access to DU case constructors to python code

Open smoothdeveloper opened this issue 2 years ago • 5 comments

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

smoothdeveloper avatar Oct 21 '23 11:10 smoothdeveloper

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

smoothdeveloper avatar Oct 22 '23 11:10 smoothdeveloper

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.

smoothdeveloper avatar Oct 22 '23 11:10 smoothdeveloper

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 avatar Oct 23 '23 08:10 dbrattli

@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

smoothdeveloper avatar Oct 27 '23 23:10 smoothdeveloper

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.

dbrattli avatar Oct 28 '23 18:10 dbrattli