Skip to content

Instantly share code, notes, and snippets.

@olleharstedt
Last active October 7, 2022 21:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save olleharstedt/88752595d8abb0ff7ba7197d26b3d15b to your computer and use it in GitHub Desktop.
Save olleharstedt/88752595d8abb0ff7ba7197d26b3d15b to your computer and use it in GitHub Desktop.
EDSL in PHP
<?php
/**
* EDSL = embedded domain-specific language
* The domain in this example is "side-effects", anything writing/reading to io, database, file, etc
*
* Using this EDSL, you neither need DI nor mocking to make the code testable. You can dry-run the particular
* parts needed of the "effectful" logic.
*
* Inspired by tagless-final pattern from FP. https://okmij.org/ftp/tagless-final/course/optimizations.html#primer
* Kind of related pattern is event sourcing: https://eventsauce.io/docs/
*
* Usage example at bottom of file.
*/
/**
* Everything that implements Node can be evaluated by an node evaluator.
* Nodes carry only data, no behaviour. Behaviour is implemented by the different
* evaluators, so that the behaviour can be switched or mocked in testing.
*/
interface Node
{
}
class Save implements Node
{
public $model;
public function __construct($model) {
$this->model = $model;
}
}
class PushToStack implements Node
{
public $stack;
public $thing;
public function __construct($stack, $thing)
{
$this->stack = $stack;
$this->thing = $thing;
}
}
class If_ implements Node
{
public $if;
public $then;
public function __construct(Node $if)
{
$this->if = $if;
}
public function setThen(Node $if)
{
$this->then = $if;
}
}
// Wrap constructor in function to lessen boilerplate in client code.
function save($model)
{
return new Save($model);
}
// Wrap constructor in function to lessen boilerplate in client code.
function pushToStack($stack, $thing)
{
return new PushToStack($stack, $thing);
}
/**
* The state class is the node builder used by client code.
*/
class St
{
public $queue = [];
public function if($if)
{
$this->queue[] = new If_($if);
return $this;
}
public function then($if)
{
$i = count($this->queue) - 1;
if ($this->queue[$i] instanceof If_) {
$this->queue[$i]->setThen($if);
} else {
throw new InvalidArgumentException('then must come after if');
}
}
public function run($ev)
{
foreach ($this->queue as $node) {
$ev->evalNode($node);
}
}
}
// Dummy user
class User
{
public $id;
public function save()
{
throw new Exception("Database problem :(");
}
}
// Dummy request
class Request
{
public function getParam($name, $default)
{
return 1;
}
}
class NodeEvaluator
{
/**
* @return mixed
*/
public function evalNode(Node $node)
{
switch (get_class($node)) {
case "If_":
if ($this->evalNode($node->if)) {
$this->evalNode($node->then);
} elseif (!empty($node->else)) {
$this->evalNode($node->else);
}
break;
case "Save":
return $node->model->save();
break;
case "PushToStack":
$node->stack->push($node->thing);
break;
}
}
}
class DryRunEvaluator
{
public $log = [];
/**
* @return mixed
*/
public function evalNode($node)
{
switch (get_class($node)) {
case "If_":
$this->log[] = "Evaluating if";
if ($this->evalNode($node->if)) {
$this->log[] = "Evaluating then";
$this->evalNode($node->then);
} elseif (!empty($node->else)) {
$this->log[] = "Evaluating else";
$this->evalNode($node->else);
}
break;
case "Save":
$this->log[] = "Save model";
return true;
break;
case "PushToStack":
$this->log[] = "Push thing to stack: " . $node->thing;
break;
}
}
}
// --- Application code ---
// This is the function before side-effects are factored out
function originalCreateDummyUsers(Request $request): array
{
$times = $request->getParam('times', 5);
$dummyUsers = new SplStack();
for (; $times > 0; $times--) {
// Need factory to mock this line
$user = new User();
$user->username = 'John Doe';
// We want to avoid database interaction during testing.
if($user->save()) {
$dummyUsers->push($user->username);
}
}
return [
'success' => true,
'dummyUsers' => $dummyUsers
];
}
// Same function using the side-effect EDSL
function createDummyUsers(Request $request, St $st): array
{
$times = $request->getParam('times', 5);
$dummyUsers = new SplStack();
for (; $times > 0; $times--) {
$user = new User();
$user->username = 'John Doe';
// Using the EDSL
$st
->if(save($user))
->then(pushToStack($dummyUsers, $user->username));
}
return [
'success' => true,
'dummyUsers' => $dummyUsers
];
}
$st = new St();
createDummyUsers(new Request(), $st);
$dry = new DryRunEvaluator();
$st->run($dry);
echo implode("\n", $dry->log) . PHP_EOL;
@olleharstedt
Copy link
Author

olleharstedt commented Mar 1, 2022

The evaluator can be modified to return a result from the AST run. Maybe with a ReturnNode?

$result = null;
$st
  ->if(fileExists($file))
  ->then(set($result, fileGetContents($file)))
  ->else(set($result, 'DEFAULT'));
// Evaluate the AST in $st, which will store result in $result
(new Evaluator())->run($st->getAst());
$result = strtoupper($result);

Or

function getupperText(string $file, St $st)
{
    $result = 'DEFAULT';
    $st
      ->if(fileExists($file))
      ->then(set($result, fileGetContents($file)))
      ();
    return strtoupper($result);
}

https://blog.ploeh.dk/2016/09/26/decoupling-decisions-from-effects/

@olleharstedt
Copy link
Author

@olleharstedt
Copy link
Author

olleharstedt commented Oct 7, 2022

https://degoes.net/articles/modern-fp

saveFile :: Path -> Bytes -> IO Unit
saveFile p f = do
  log ("Saving file" ++ show (name p) ++ " to " ++ show (parentDir p))
  r <- httpPost ("cloudfiles.fooservice.com/" ++ (show p)) f
  if (httpOK r) then log ("Successfully saved file " ++ show p)
  else let msg = "Failed to save file " ++ show p
  in log msg *> throwException (error msg)
function saveFile(string $path, $bytes, St $st): void
{
    $failMsg = "Failed";
    $st->trace("Saving file");
    $st
        ->if(curlPost("cloudfiles.service.com" . $path, $bytes))
        ->then(trace("Successfully saved file"))
        ->else([
            trace($failedMsg),
            throw_($failedMsg)
        ]);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment