-
-
Save Crell/dc25cd173b6195af3940299651c28f5a to your computer and use it in GitHub Desktop.
Just noodling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// This is an Enum. | |
enum Direction { | |
// This is an Enum Member. | |
// Specifically, these are Unit Members, because they have no associated data. | |
case North; | |
case South; | |
// Like class properties can be single-lined, so can Unit Members. | |
case East, West; | |
} | |
// Unqualified Enum Members are never permitted. | |
// This may sometimes be a bit more verbose, as in this case, | |
// but is probably better for the parser ad overall understandability. | |
function move(Direction $dir = Direction::North): string | |
{ | |
// Because Unit Members are singletons, they are always === to themselves. | |
// Thus match() works on them as is with no further changes. | |
return match ($dir) { | |
Direction::North => "forward", | |
Direction::South => "backward", | |
Direction::East => "right", | |
Direction::West => "left", | |
}; | |
// If the value being matched is an enum, you can explicitly toggle on exhausive checks: | |
return match enum ($dir) { | |
Direction::North => "forward", | |
Direction::South => "backward", | |
Direction::East => "right", | |
}; | |
// This will error out, as PHP knows it should be matching an enum and therefore be | |
// exhaustive. That allows us to not have to make match() magically smart about what | |
// it's matching, which sounds hard to me. | |
} | |
function list_moves() | |
{ | |
// Direction::list() is a static method. I realized that getIterator() only makes sense | |
// on objects, and Direction is not an object. So we need a special method. | |
// Part of me isn't sure we even need this, frankly. | |
foreach (Direction::list() as $direction) { | |
// By default, prints the name of the Enum Member as a string. | |
// So one of North, South, East, West. | |
// I know Anthony argued that it should have an explicit value() method, | |
// but __toString is well established and Python uses its equilvalent. | |
print $direction; | |
} | |
} | |
// Another possibility is to make Enums have to opt-in to being enumerable, and then | |
// parse fail if any of the members take discriminated values. | |
#[Enumerable] | |
enum Direction { case North, South, East, West; } | |
// This would parse fail. | |
#[Enumerable] | |
enum Distance { | |
case Kilometers(int $num); | |
case Miles(int $num); | |
} | |
// Enums can have fixed Associated Values connected with them. In that case, the Member is still a singleton, and thus | |
// === still holds, and it is iterable (in whatever way we do that, as above). | |
// There are two ways to make it possible to read the associated value: Pattern matching or requiring named parameters | |
// in the declaration. I've shown both options here: | |
// Pattern matching. | |
enum Suit { | |
case Hearts('H'); | |
case Diamonds('D'); | |
case Clubs('C'); | |
case Spades('S'); | |
public function color() { | |
return match ($this) { | |
Suit::Hearts, Suit::Diamonds => 'Red', | |
Suit::Clubs, Suit::Spades: 'Black' | |
}; | |
} | |
} | |
match enum ($suit) { | |
Suit::Hearts($a) => $a, | |
Suit::_($a) => $a, // Means any Member of Suit. | |
} | |
enum Tree { | |
// This implicitly puts a public $val on an instance of Tree::Leaf. | |
// It can be made private by using Leaf(private $val), compact-constructor style. | |
case Leaf(protected $val) { | |
// I'd love to make this a short function syntax. Separate RFC. | |
public function fmap(callable $f) | |
{ | |
return $f($this->val); | |
} | |
}; | |
// Ideally, enums should be able to have recursive values. Off hand I see no | |
// reason why this wouldn't be straightforward. | |
case Node(Tree $left, Tree $right) { | |
// In practice, whether a method is best implemented separately in each Member or | |
// as a single method with match() is up to the user. I'd likely recommend "single | |
// method if it's all one-liners, separate methods if they are too big for the single-line | |
// requirement of match()." | |
public function fmap(callable $f): Tree | |
{ | |
// This poses an interesting question. Do you create a new instance of an enum with `new`, | |
// or is it necessarily a static constructor? And if a static constructor, do we | |
// allow multiple static constructors for a single Enum Member, or do we call that | |
// "Use a damned class, buddy" territory? My thinking is that there's a single static method | |
// and if you need anything else, use a class. | |
return Tree::Node($f($this->left), $f($this->right)); | |
} | |
}; | |
// Forces both Enum Members to have this method. I am still undecided about how tightly to control | |
// method consistency between Members. | |
abstract public function fmap($callable $f); | |
public function value() | |
{ | |
return match($this) { | |
Tree::Leaf => $this->value, | |
Tree::Node => [...$left->value(), ...$right-value()], | |
}; | |
} | |
} | |
// For fun, here's what it would look like with all comments removed and all possible shortening. | |
enum Tree { | |
case Leaf(protected $val) { | |
public function fmap(callable $f) => $f($this->val); | |
}; | |
case Node(Tree $left, Tree $right) { | |
public function fmap(callable $f) => Tree::Node($f($this->left), $f($this->right)); | |
}; | |
abstract public function fmap($callable $f); | |
public function value() => match($this) { | |
Tree::Leaf => $this->val, | |
Tree::Node => [...$left->value(), ...$right-value()], | |
}; | |
} | |
enum Optional { | |
// This is a Unit Member. | |
case None; | |
// This is a Member with Associated Value. | |
case Some(private $val); | |
public function value(): mixed | |
{ | |
return match ($this) { | |
Optional::None => throw new Exception(), | |
Optional::Some => $this->val, | |
}; | |
} | |
// Note that the return type can be the Enum itself, thus restricting the return | |
// value to one of the enumerated types. | |
public function fmap(callable $f): Optional | |
{ | |
return match ($this) { | |
Optional::None => $this, | |
Optional::Some => Optional::Some($f($this->val)), | |
}; | |
} | |
// I would still love to turn this into a magic method with dedicated operator, | |
// but that's a separate issue. | |
// A return type of "static" means Optional in this case. If the method were defined inside | |
// Some instead, it would mean Some. | |
public function bind(callable $f): static | |
{ | |
return match ($this) { | |
Optional::None => $this, | |
Optional::Some => $f($this->val), | |
}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment