Skip to content

Instantly share code, notes, and snippets.

@olleharstedt
Last active February 14, 2022 18:19
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/5480aa4efc669dcdc99bd1ee909c143f to your computer and use it in GitHub Desktop.
Save olleharstedt/5480aa4efc669dcdc99bd1ee909c143f to your computer and use it in GitHub Desktop.

Pre-req: Functional core, imperative shell

Pre-req: Side-effects, purity, referential transparency

  • The functional core is more testable and more composable than the imperative shell or effectful code
  • Extending the functional core means making a bigger part of the software pure
  • Side-effects can in some cases easily be lifted out from a function, creating a more composable and testable unit
  • But sometimes, side-effects are tangled in business logic
  • Some side-effects can be delayed until end of request (e.g., updating user's name in database; exception at failure can still be thrown)
  • Example: Create dummy users
/**
 * Alternatives:
 *
 * 1) Naive (implicit depedencies, everything must be integrity tested)
 * 2) Injected implementation (factories)
 * 3) Queue implementation (this implementation; side-effects are delayed or "delegated" to the queue)
 */
function actionCreateDummyUsers(Request $request)
{
    $times  = $request->getParam('times', 5);
    $prefix = $request->getParam('prefix', 'randuser_');
    $email  = $request->getParam('email', 'no@email.com');

    $randomUsers = [];

    for (; $times > 0; $times--) {
        que(
            function (PasswordManagement $pwm, User $user) use ($prefix, $email, &$randomUsers) {
                $password         = $pwm->getRandomPassword();
                $name             = $user->getRandomUsername($prefix);
                $user->users_name = $name;
                $user->full_name  = $name;
                $user->email      = $email;
                $user->created    = date('Y-m-d H:i:s');
                $user->modified   = date('Y-m-d H:i:s');
                $user->password   = password_hash($password, PASSWORD_DEFAULT);
                if ($user->save()) {
                    $randomUsers[] = ['username' => $name, 'password' => $password];
                }
            }
        );
    }

    return [
        'success'     => true,
        'randomUsers' => &$randomUsers,
        'filename'    => $prefix
    ];
}

// Usage:
$result = actionCreateDummyUsers(new Request());
$queue = que(null);
$fn = $queue->pop();
$fn(new PasswordManagement(), new User());
var_export($result);
/*
array (
    'success' => true,
    'randomUsers' =>
    array (
        0 =>
        array (
            'username' => '1abc',
            'password' => '123',
        ),
    ),
    'filename' => 1,
)
*/

// Simple queue class
class Queue
{
    /** @var callable[] */
    private $fns = [];

    public function __construct(array $fns)
    {
        $this->fns = $fns;
    }

    // Run all callables
    // TODO: Autowire with reflection
    public function run()
    {
        array_walk($this->fns, fn ($fn) => $fn());
    }

    public function pop()
    {
        return array_pop($this->fns);
    }
}

// Simple que() function
function que(callable|null $fn)
{
    static $fns = [];
    if (is_callable($fn)) {
        $fns[] = $fn;
    } else {
        return new Queue($fns);
    }
}

// Dummy user, save() must be mocked in unit-test
class User
{
    public $id;

    public function getRandomUsername(string $prefix)
    {
        return $prefix . 'abc';
    }

    public function save()
    {
        return true;
    }
}

// Dummy request
class Request
{
    public function getParam($name, $default)
    {
        return 1;
    }
}

// Dummy class
class PasswordManagement
{
    public function getRandomPassword()
    {
        return '123';
    }
}
@olleharstedt
Copy link
Author

@olleharstedt
Copy link
Author

olleharstedt commented Feb 12, 2022

que(new IO.if(fn => DB.getUser).then(fn => save new username)else(fn => show error))

@olleharstedt
Copy link
Author

olleharstedt commented Feb 12, 2022

@olleharstedt
Copy link
Author

$st = new St();

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';

        $st
            ->if(fn () => $user->save())
            ->then(fn () => $dummyUsers->push(['username' => $user->username]));
    }

    return [
        'success'    => true,
        'dummyUsers' => $dummyUsers
    ];
}

$res = createDummyUsers(new Request(), $st);
$st->queue[0]->runThen();
var_export($res);

// Classes below

class If_
{
    private $fn;
    private $then;

    public function __construct(callable $fn)
    {
        $this->fn = $fn;
    }

    public function setThen(callable $fn)
    {
        $this->then = $fn;
    }

    public function run()
    {
        $fn = $this->fn;
        $res = $fn();
        if ($res && $this->then) {
            $then = $this->then;
            $then();
        }
    }

    public function runThen()
    {
        $then = $this->then;
        $then();
    }
}

class St
{
    public $queue = [];

    public function if(callable $fn)
    {
        $this->queue[] = new If_($fn);
        return $this;
    }

    public function then(callable $fn)
    {
        $i = count($this->queue) - 1;
        if ($this->queue[$i] instanceof If_) {
            $this->queue[$i]->setThen($fn);
        } else {
            throw new InvalidArgumentException('then must come after if');
        }
    }

    public function run()
    {
        foreach ($this->queue as $q) {
            $q->run();
        }
    }
}

// Dummy user, save() must be mocked in unit-test
class User
{
    public $id;

    public function save()
    {
        throw new Exception("Database problem :(");
    }
}

// Dummy request
class Request
{
    public function getParam($name, $default)
    {
        return 2;
    }
}

// Dummy class
class PasswordManagement
{
    public function getRandomPassword()
    {
        return '123';
    }
}

@olleharstedt
Copy link
Author

Counter args:

  • Harder to debug
  • Non-idiomatic
  • Switched mock DSL (PHPUnit mock builder) to effect DSL (effect builder)

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