Proposed alias for union types

The | character is used in a few different languages as a concise way to specify type unions, like in TypeScript, Scala, PHP 8+, Python 3.10+, and (sort of) Haskell. I was curious to hear what people’s initial thoughts would be on having this as a concise way to create Unions. This would be completely backwards-compatible of course; Union by itself would still be Union.

Currently | is used for OR, which has close connections to type unions in formal logic (h/t @mkitti in this). Because of this connection, | is used for both OR and union in TypeScript, Scala, PHP, and Python.

Now, would it be possible to add this? From what I can see, there are no potentially ambiguous methods in Base:

Current methods: (expand)
  [1] |(a::FileWatching.FileEvent, b::FileWatching.FileEvent)
     @ FileWatching ~/.julia/juliaup/julia-1.10.0+0.aarch64.apple.darwin14/share/julia/stdlib/v1.10/FileWatching/src/FileWatching.jl:48
  [2] |(b::Bool, a::Missing)
     @ missing.jl:175
  [3] |(x::Bool, y::Bool)
     @ bool.jl:39
  [4] |(a::Missing, b::Bool)
     @ missing.jl:174
  [5] |(::Missing, ::Missing)
     @ missing.jl:173
  [6] |(::Missing)
     @ missing.jl:101
  [7] |(::Missing, ::Integer)
     @ missing.jl:176
  [8] |(a::BigInt, b::BigInt, c::BigInt, d::BigInt, e::BigInt)
     @ Base.GMP gmp.jl:543
  [9] |(a::BigInt, b::BigInt, c::BigInt, d::BigInt)
     @ Base.GMP gmp.jl:542
 [10] |(a::BigInt, b::BigInt, c::BigInt)
     @ Base.GMP gmp.jl:541
 [11] |(x::BigInt, y::BigInt)
     @ Base.GMP gmp.jl:501
 [12] |(a::FileWatching.FDEvent, b::FileWatching.FDEvent)
     @ FileWatching ~/.julia/juliaup/julia-1.10.0+0.aarch64.apple.darwin14/share/julia/stdlib/v1.10/FileWatching/src/FileWatching.jl:79
 [13] |(::Integer, ::Missing)
     @ missing.jl:177
 [14] |(x::T, y::T) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}
     @ int.jl:372
 [15] |(x::T, y::T) where T<:Integer
     @ promotion.jl:518
 [16] |(a::Integer, b::Integer)
     @ int.jl:1064
 [17] |(x::Integer)
     @ operators.jl:527
 [18] |(a, b, c, xs...)
     @ operators.jl:587

So it should be backwards compatible to implement for union types like this:

Base.:|(T1::Union{Type,TypeVar}, T2::Union{Type,TypeVar}) = Union{T1,T2}

This is literally the entirety of the implementation. This lets you do things like:

julia> Float32 | Float64
Union{Float32, Float64}

julia> Int8 | Int16 | Int32
Union{Int16, Int32, Int8}

julia> Vector{<:AbstractFloat} | Matrix{<:AbstractFloat}
Union{Matrix{<:AbstractFloat}, Vector{<:AbstractFloat}} # (alias for Union{Array{<:AbstractFloat, 1}, Array{<:AbstractFloat, 2}})

as well as in method definitions:

julia> function add(a::(Float32|Float64), b::(Float32|Float64))
           a + b
       end
add (generic function with 1 method)

julia> add(1.0, 1f0)
2.0

To me personally I find this “reads” better than

julia> function add(a::Union{Float32,Float64}, b::Union{Float32,Float64})
           a + b
       end

because it’s a bit closer to how I would describe this function in language: “add takes float-32 or float-64”, rather than “add takes the union of float-32 and float-64”.

This is especially true for nested types, like

julia> f(a::Tuple{Float32|Float64, Float32|Float64}) = (a[1]^2, a[2]^2)

compared to

julia> f(a::Tuple{Union{Float32,Float64}, Union{Float32,Float64}}) = (a[1]^2, a[2]^2)

Union is a frequently occuring symbol in Julia code; actually much more frequent than many existing infix operators (see this). A common use-case in data science is tables of numerical data with missing values, like:

Schema{Union{Float32,Missing},Union{String,Missing},Union{Int64,Missing}}

This gets simplified to:

Schema{(Float32|Missing),(String|Missing),(Int64|Missing)}

or in vectors like Vector{Real|Complex|Missing}. The parentheses are a stylistic choice but aren’t technically needed.

This can also be used for setting defaults:

f(; cleanup::Union{Bool,Nothing}=nothing) = ...

which becomes

f(; cleanup::(Bool|Nothing)=nothing) = ...

which notably moves the Bool closer to the ::. Quickly scanning the code, it’s easier to see that cleanup is a boolean.

This syntax also has connections to existing Regex syntax, like:

julia> occursin(r"(A|B)", "A")
true

Which checks for matches to A or B.

Just curious to hear what people think and initial reactions.

I couldn’t find any existing discourse threads, but it seems @Mason had suggested this syntax in a PR comment in 2020, but it got buried in the main discussion (h/t @mbauman). As to why | was not suggested earlier in Julia’s history, it is likely because it has only recently become dominant in other languages for representing unions.


  • Edit: removed comment about operator precedence
  • Edit 2: updated main implementation
  • Edit 3: updated key text
  • Edit 4: pointed out initial suggestion on GH
  • Edit 5: added more examples
  • Edit 6: Changed Base.:|(t::Type, types::Type...) into Base.:|(::Type{A}, ::Type{B}) to reduce surface
  • Edit 7: added regexp example
  • Edit 8: Fixed definition to also include TypeVar which is valid input to Union
29 Likes

Coming from languages like typescript, i naturally started using Union in most methods to create a kind of “generic” function, and i also had the idea to define the | operator for creating union types, but later i realized that instead of using union types, you use an abstract type to make the function generic, and later if you want specifc behavior for a concrete sub type you define a method for it, letting dynamic dispatch do its job.

3 Likes

If you can use a union type instead of an abstract type it’s usually the better option

1 Like

@dylanxyz not quite sure where you are going with your comment but I should say that I am not aiming for a discussion of the use-cases of union types… they are without a doubt one of the most fundamental building blocks of Julia code, both in the standard library and the ecosystem. I’m more interested in just discussing this particular syntax!

2 Likes

Isn’t the union symbol, rather than |?

9 Likes

I could see that being an alternative option to consider!

Personally I might favor | for a few reasons, but I’m interested in hearing what you think:

  • Has precedent in other modern languages for declaring type unions
  • 3 fewer keystrokes (\cup<tab> vs <shift>\)
  • Unicode characters can cause some issues due to their lack of built-in support on some platforms, whereas | is compatible with everything. (e.g., a phone keyboard, GitHub search, a new cloud server’s vim, etc). For an operator that could become widely used I think this is more sensitive of an issue.
  • I’m not sure what other people actually “read” when they read a signature, but f(x::Union{Float32,Float64}) I would describe as taking “float32 or float64”, rather than a “union of float32 and float64”. (But this could totally be a personal thing)

The benefit I see is maybe less ambiguity in the mathematical meaning, since “\cup” has the correct meaning in math over sets (although perhaps incorrect in that concrete types can also be included in a union — which is more like an element), whereas in math, “|” is more like “given”. But I feel like “|” seems to have a different meaning in programming languages — one which actually is type unions…? What do you think?

I’m cognizant that syntax choices like this are purely based on popular opinion, though, since it’s what the majority finds intuitive/useful/etc. So this is why I’m very interested in seeing what people think.

6 Likes

I agree with your points here @MilesCranmer and come to a similar conclusion – if we were to add a more concise operator for Union, | is more intuitive because it’s used for a similar purpose in other languages.

I don’t want to dredge up the argument about * concatenation, but my bias is towards picking colloquial intuition over mathematical integrity.

5 Likes

I wouldn’t want another syntax that doesn’t add any functionality, and Union{...} is already concise enough that I wouldn’t want to lose the parallel between writing and printing. The | chain in particular must contend with operator precedence and may need parentheses like the example’s a::(Float32|Float64), and A | B requires 2 inputs. You can repeat A | A to replicate Union{A} === A, but what replicates Union{} or Union{typesiterable...}? I say better to leave this operator open to work on types in different ways in third-party libraries, maybe even concrete sum types.

5 Likes

I don’t see this as a concern imo. Union is still the underlying type; | is simply used as a convenience method for specifying it. You can always write out the type explicitly if needed, such as for things like Union{}, or if it’s just a personal preference.

I would say that it’s quite idiomatic in julia to define convenience operators for types as alternatives to writing out a specific function name for each type and behavior. A close example in Base is * applied to strings: Base.:*(a::String...) calls string(a...) to do concatenations — but you could also call string directly. So this is simply defining | on the space of Type: using it to mean “this type or this type”, rather than its standard behavior on Bool of “this bit or this bit”.

You could do (|)(typesiterable...) for that example, though it’s probably more readable to write out the Union explicitly in that case.

5 Likes

Bitwise or isn’t just for Bool, and it doesn’t mean a union of its inputs. 3 | 5 does not mean any member of the set {3, 5}, it very much must be 7. There’s no analogy to Union{Int, Union{Float64, Bool}} == Union{Bool, Float64, Int64}.

Granted, overloaded operators don’t need a common thread. * for strings is not scalar multiplication, that in turn is not matrix multiplication. I just don’t see any good reason to use an operator in a new way for what I don’t agree to be convenient. * was used for strings before v1.0 and after years of consideration; convenience operators aren’t made regularly even in third-party libraries, let alone Base. Points for precedence in other languages, but I prefer Union.

Agreed, also not a fan because these and a few other operators really do afoldl on binary operations, involving somewhat mitigated performance problems; from what I can tell in operators.jl, afoldl optimizes up to 31 inputs. Chained a | b | c itself lowers to a sequence of binary operations, unlike + or *, so I don’t think it runs into afoldl, but I don’t like all the intermediate Unions. Union{...} does Core.apply_type(Union, ...), but I can’t read builtins.c.

1 Like

I’m not sure I follow this point, sorry. To clarify I am not suggesting | is literally a bitwise or for Type. There’s simply a conceptual similarity here, in that a ∈ Union{A,B} => (a ∈ A) | (a ∈ B).

But it doesn’t have the same algebraic properties, the same way * doesn’t have the same algebraic properties for all input types.

Thanks for sharing your preferences though!

Not sure performance problems are an issue because presumably the compiler would just inline everything and create union types at compile time? But you’re right it’s probably worth checking.

e.g.,

julia> Base.:|(t::Type...) = Union{t...}

julia> f(a, b) = typeof(a) | typeof(b)
f (generic function with 1 method)

julia> f(1.0, 1f0)
Union{Float32, Float64}

julia> @code_llvm f(1.0, 1f0)
;  @ REPL[3]:1 within `f`
define nonnull {}* @julia_f_360(double %0, float %1) #0 {
top:
  ret {}* inttoptr (i64 4453951664 to {}*)
}

You do get my point. You’re using | for types like a union operation, which bitwise or is not. I was more or less repeating what you said, but it suits my preferences less.

Your example has 2 inputs, which is well below the afoldl threshold, and it dodges afoldl by defining |(t::Type...) specifically for types instead of defining the binary operation and using the generic |(a, b, c, xs...). That bit of indirection seems to have some overhead, which I wouldn’t want to add even to compile-time (header annotations would happen earlier at when the definition is evaluated), but it’s small enough that it’s not really a reason for my opposition.

julia> types3 = (Int, Float64, Bool)
(Int64, Float64, Bool)

julia> types40 = typeof.(ones(40));

julia> @time Union{types3...} # no need to compile, I suppose
  0.000009 seconds (2 allocations: 64 bytes)
Union{Bool, Float64, Int64}

julia> @time Union{types40...}
  0.000006 seconds
Float64

julia> foo(t::Type...) = Union{t...} # I'm avoiding type piracy here
foo (generic function with 1 method)

julia> @time foo(types3...)
  0.002380 seconds (605 allocations: 35.546 KiB, 98.61% compilation time)
Union{Bool, Float64, Int64}

julia> @time foo(types3...)
  0.000012 seconds (8 allocations: 368 bytes)
Union{Bool, Float64, Int64}

julia> @time foo(types40...)
  0.000033 seconds (6 allocations: 1.219 KiB)
Float64

Yeah it basically looks like there’s nothing to worry about with regards to compile time or type inference:

julia> Base.:|(t::Type...) = Union{t...}

julia> f(a...) = (|)(typeof.(a)...)  # With |
f (generic function with 1 method)

julia> g(a...) = Union{typeof.(a)...}  # With Union
g (generic function with 1 method)

julia> # Sample 100 random types for input (so each run is a new compile)
       @btime f(random_types_100...) setup=(random_types_100=one.(tuple(rand((UInt8,UInt16), 100))...)) evals=1 samples=100
  23.705 ms (326400 allocations: 22.35 MiB)
Union{UInt16, UInt8}

julia> @btime g(random_types_100...) setup=(random_types_100=one.(tuple(rand((UInt8,UInt16), 100))...)) evals=1 samples=100
  23.818 ms (326397 allocations: 22.35 MiB)
Union{UInt16, UInt8}

(Presumably the difference is just noise)

Which produce identical LLVM (expand)
julia> @code_llvm f(one.(tuple(rand((UInt8,UInt16), 100))...)...)
;  @ REPL[2]:1 within `f`
define nonnull {}* @julia_f_1449(i16 zeroext %0, i16 zeroext %1, i16 zeroext %2, i8 zeroext %3, i16 zeroext %4, i16 zeroext %5, i16 zeroext %6, i16 zeroext %7, i16 zeroext %8, i8 zeroext %9, i16 zeroext %10, i8 zeroext %11, i8 zeroext %12, i8 zeroext %13, i16 zeroext %14, i8 zeroext %15, i8 zeroext %16, i16 zeroext %17, i16 zeroext %18, i16 zeroext %19, i16 zeroext %20, i8 zeroext %21, i16 zeroext %22, i8 zeroext %23, i8 zeroext %24, i16 zeroext %25, i8 zeroext %26, i16 zeroext %27, i8 zeroext %28, i16 zeroext %29, i8 zeroext %30, i8 zeroext %31, i16 zeroext %32, i8 zeroext %33, i8 zeroext %34, i8 zeroext %35, i16 zeroext %36, i8 zeroext %37, i16 zeroext %38, i16 zeroext %39, i8 zeroext %40, i8 zeroext %41, i8 zeroext %42, i8 zeroext %43, i8 zeroext %44, i16 zeroext %45, i8 zeroext %46, i16 zeroext %47, i16 zeroext %48, i8 zeroext %49, i8 zeroext %50, i8 zeroext %51, i8 zeroext %52, i16 zeroext %53, i8 zeroext %54, i16 zeroext %55, i8 zeroext %56, i16 zeroext %57, i8 zeroext %58, i16 zeroext %59, i16 zeroext %60, i16 zeroext %61, i8 zeroext %62, i16 zeroext %63, i16 zeroext %64, i8 zeroext %65, i8 zeroext %66, i16 zeroext %67, i16 zeroext %68, i16 zeroext %69, i8 zeroext %70, i16 zeroext %71, i8 zeroext %72, i16 zeroext %73, i16 zeroext %74, i8 zeroext %75, i16 zeroext %76, i8 zeroext %77, i8 zeroext %78, i8 zeroext %79, i8 zeroext %80, i8 zeroext %81, i16 zeroext %82, i16 zeroext %83, i16 zeroext %84, i8 zeroext %85, i16 zeroext %86, i16 zeroext %87, i8 zeroext %88, i16 zeroext %89, i16 zeroext %90, i16 zeroext %91, i8 zeroext %92, i8 zeroext %93, i8 zeroext %94, i8 zeroext %95, i16 zeroext %96, i16 zeroext %97, i8 zeroext %98, i8 zeroext %99) #0 {
top:
  ret {}* inttoptr (i64 4516095248 to {}*)
}

julia> @code_llvm g(one.(tuple(rand((UInt8,UInt16), 100))...)...)
;  @ REPL[3]:1 within `g`
define nonnull {}* @julia_g_1453(i16 zeroext %0, i8 zeroext %1, i8 zeroext %2, i16 zeroext %3, i8 zeroext %4, i8 zeroext %5, i8 zeroext %6, i16 zeroext %7, i16 zeroext %8, i8 zeroext %9, i8 zeroext %10, i16 zeroext %11, i16 zeroext %12, i8 zeroext %13, i16 zeroext %14, i8 zeroext %15, i16 zeroext %16, i16 zeroext %17, i16 zeroext %18, i8 zeroext %19, i8 zeroext %20, i16 zeroext %21, i16 zeroext %22, i8 zeroext %23, i16 zeroext %24, i16 zeroext %25, i16 zeroext %26, i8 zeroext %27, i8 zeroext %28, i8 zeroext %29, i8 zeroext %30, i8 zeroext %31, i16 zeroext %32, i16 zeroext %33, i8 zeroext %34, i16 zeroext %35, i8 zeroext %36, i16 zeroext %37, i8 zeroext %38, i16 zeroext %39, i8 zeroext %40, i16 zeroext %41, i16 zeroext %42, i8 zeroext %43, i8 zeroext %44, i8 zeroext %45, i16 zeroext %46, i8 zeroext %47, i16 zeroext %48, i8 zeroext %49, i16 zeroext %50, i8 zeroext %51, i8 zeroext %52, i16 zeroext %53, i16 zeroext %54, i8 zeroext %55, i8 zeroext %56, i8 zeroext %57, i8 zeroext %58, i8 zeroext %59, i16 zeroext %60, i16 zeroext %61, i16 zeroext %62, i8 zeroext %63, i8 zeroext %64, i16 zeroext %65, i8 zeroext %66, i8 zeroext %67, i8 zeroext %68, i8 zeroext %69, i8 zeroext %70, i8 zeroext %71, i8 zeroext %72, i16 zeroext %73, i8 zeroext %74, i16 zeroext %75, i8 zeroext %76, i16 zeroext %77, i8 zeroext %78, i8 zeroext %79, i16 zeroext %80, i16 zeroext %81, i8 zeroext %82, i8 zeroext %83, i16 zeroext %84, i8 zeroext %85, i16 zeroext %86, i16 zeroext %87, i16 zeroext %88, i16 zeroext %89, i8 zeroext %90, i16 zeroext %91, i16 zeroext %92, i8 zeroext %93, i8 zeroext %94, i8 zeroext %95, i8 zeroext %96, i16 zeroext %97, i8 zeroext %98, i8 zeroext %99) #0 {
top:
  ret {}* inttoptr (i64 4516095248 to {}*)
}
1 Like

Those both do Union indirectly the same way, and I think your benchmark also measures additional splatting and broadcasted typeof. There is overhead compared to broadcasting typeof, and splatting into Union directly:

julia> @btime g(random_types_100...) setup=(random_types_100=one.(tuple(rand((UInt8,UInt16), 100))...)) evals=1 samples=100
  110.255 ms (449099 allocations: 25.69 MiB)
Union{UInt16, UInt8}

julia> @btime Union{typeof.(random_types_100)...} setup=(random_types_100=one.(tuple(rand((UInt8,UInt16), 100))...)) evals=1 samples=100
  12.310 μs (10 allocations: 1.23 KiB)
Union{UInt16, UInt8}

I am not entirely sure if this compile-time work actually happens; your @btime results suggest otherwise. Variable arguments aren’t specialized automatically, and the @code_ reflection functions/macros assume specialization too often. You can tell from @code_warntype on a few calls of g, the input type changes each time.

1 Like

Oh I think I get what you are saying. But I think this would always be inlined to Union{t...}, if not evaluated at compile time, anyways (presuming the calling function is specialized) so would this even matter?

Or are you saying it would be better to have something like

Base.:|(ts::Vararg{Type,N}) where {N} = Union{ts...}

to force specialization? Or even

Base.:|(::Type{T1}, ::Type{T2}) where {T1,T2} = Union{T1,T2}
Base.:|(ts::Vararg{Type,N}) where {N} = Union{ts...}

to force type specialization for the binary version?

I could see this quickly blowing up the number of methods though.

1 Like

IMO this isn’t enough. Technically Base is allowed to add new methods to the | function, but this would be confusing, because it would add new, more-or-less unrelated semantics to the function. This would result in unnecessary burden on:

  1. The Julia developers, who have to keep testing and documenting Base.:| in perpetuity

  2. The user, who somewhat has to keep the different semantics in their head

Perhaps going with another symbol, like suggested by @jar1, would be a good idea.

2 Likes

Arguably, PSA: Julia is not at that stage of development anymore applies here. Union{...} has been a basic part of Julia’s syntax for more than 10 years now, is explicit and reasonably concise, and doesn’t seem to have been a major point of difficulty (no one has even aired a desire for an alternate syntax to my knowledge?). Adding an alternate syntax (especially if it involves changing operator precedence!) is probably not realistically on the table, even though it can be fun to think about.

13 Likes

Just to quickly correct something — please ignore the operator precedence comment I made as an aside… I didn’t anticipate anyone focusing on that. I can remove that from the post as it was really not a central part of the idea.

Edit: removed that part

4 Likes

I couldn’t find anything either. And that is exactly why I made this thread! This isn’t a concrete proposal, I’m just interested in discussing the idea and testing the waters.

Regarding @StefanKarpinski’s thread, I want to emphasize that the idea discussed here is not to change any existing syntax: it’s to add a new method, which I would think (?) is compatible with Julia 1.x’s syntax freeze –

So, Union{...} would still be the type, this is just a new alternative for constructing them. (An analagous situation happened for Python 3.9 → 3.10 [here] when they introduced | as an alternative to their existing Union[] type hint, which happens to also be bitwise-or for bools and integers.) It’s not a breaking change, just a new method. Since | currently throws an error on ::Type input, this would not affect existing code.

4 Likes

It is not clear to me that the PSA necessarily applies here. We are not trying to invalidate the prior Union{...} syntax. An alias has been proposed based on the use in other languages to create union types.

As far as I can the alias would not be a breaking change. Prior code would be expected to still function. Union{} is still necessary. |() meaning Union{} seems problematic as it could easily occur unintentionally. I think the alias Base.Bottom may also be sufficient and increase in usage if this introduced.

Because of this, I would revise the definiton to be as follows.

Base.:|(t::Type, types::Type...) = Union{t, types...}

Objectives

The objectives here are two fold.

  1. Increase accessibility of the Julia Language to those using other typed languages.
  2. Increase overall readability and comprehension.

The popularity of this syntax has arisen concurrent with the development of Julia. TypeScript is about as old as Julia, I believe. Python adopted this syntax in 2019. Just as Python syntax has evolved over time, it is worth considering if Julia syntax should also involve.

This thread is that exact airing. Mile’s work sits at the intersection of Julia and Python with his package SymbolicRegression.jl and its Python interface PySR.

This point is now in question. | is considerably shorter than Union{...}. The lack of this in Julia makes Julia seem verbose, especially since typing is central to Julia’s multiple dispatch paradigm.

Survey of Languages

Existing Syntax

I also just found similar feature requests arising in Cython and Mojo.

Feature Requests

  1. Cython: [ENH] Implement PEP-604 (union types as X | Y) as fused types · Issue #4631 · cython/cython · GitHub
  2. Mojo: [Feature Request] Implement union types (rather than nominal enums) · Issue #43 · modularml/mojo · GitHub

Parsing

The operator precedence issue is the most concerning. There is also a parser issue. While separable from the main proposal, it is an important consideration nonetheless.

julia> 0x8::Int8 | UInt8
ERROR: TypeError: in typeassert, expected Int8, got a value of type UInt8

julia> 0x8::UInt8 | Int8                                              
ERROR: MethodError: no method matching |(::UInt8, ::Type{Int8})

julia> f(::Int8 | UInt8) = 1 # Julia 1.9
ERROR: syntax: "(::Int8 | UInt8)" is not a valid function argument name around REPL[29]:1

The particular issue here is that while we may be able to expand the use of | for types, the syntax is similar yet fundamentally distinct than in the other languages. While the function definition parser issue could be addressed, the type assertion syntax seems to be a more fundamental challenge and would create consistency issues.

While the parentheses does clarify the syntax, it diminishes the conciseness.

Because this involves syntax issues, I would particularly like to hear from @c42f on this matter.

Summary

Since the addition of Base.:|(t::Type, types::Types...) = Union{t, types...} would be a non-breaking change, I do not think we should dismiss this on language maturity grounds. Julia’s syntax should be able to evolve over time and respond to trends in other languages where they make sense. In particular, the proposed syntax is more concise than existing syntax and has gained adoption across a number of contemporary languages.

Adopting the syntax may increase overall comprehension by developers, especially those with exposure to other languages. Issues of parsing and operator precedence should be carefully considered as they diminish the motivation in part.

Overall, I believe the proposal is pertinent and should be carefully studied.

edit: Added feature requests in Cython and Mojo. Also added a link for PHP.

17 Likes