【问题标题】:How can I implement "thunks" (delayed computation) in a general way using Moo and Type::Tiny?如何使用 Moo 和 Type::Tiny 以一般方式实现“thunk”(延迟计算)?
【发布时间】:2018-02-01 19:38:27
【问题描述】:

我希望能够拥有一个具有以下特征的 Moo* 类:

  • 对象的属性可以存储对对象本身的引用
  • 该属性将使用 Type::Tiny 类型进行类型约束,因此引用必须是正确的类型
  • 当类是不可变的并且属性是“必需的”时,它必须起作用,即未定义的值是不可接受的,以后不能更新

例如

package GraphQLType;
use Moo;
use Types::Standard -all;
has [qw(children)] => (
  is => 'rwp',
  isa => ArrayRef[InstanceOf['GraphQLType']],
  required => 1,
);

package main;
my $type;
$type = GraphQLType->new(children => [$type]);

上面提出了一个先有鸡还是先有蛋的问题:$type 将是未定义的,因此无法通过类型约束。

graphql-js 中使用的模式是"thunking"。在 Perl 术语中:

package GraphQLType;
use Moo;
use Types::Standard -all;
has [qw(children)] => (
  is => 'rwp',
  isa => CodeRef | ArrayRef[InstanceOf['GraphQLType']],
  required => 1,
);

package main;
my $type;
$type = GraphQLType->new(children => sub { [$type] });

虽然这适用于那里的特定类型,但我怎样才能拥有一个实现类似这样的参数化类型?此外,如果这可以与“惰性”功能挂钩,以最大限度地减少存储计算值所涉及的代码,它将更有帮助。

package Thunking;

use Moo;
use Types::Thunking -all;
use Types::Standard -all;

has [qw(children)] => (
  is => 'lazy',
  isa => Thunk[ArrayRef[InstanceOf['GraphQLType']]],
  required => 1,
);

【问题讨论】:

    标签: perl oop moo


    【解决方案1】:

    这里需要处理两个问题:延迟计算不可变属性 (DCIA) 的参数化 Type::Tiny 类型约束和实际运行的 DCIA。

    参数化类型

    因为这是 Perl,所以有不止一种方法可以做到这一点。在Type::Tiny 中创建参数化类型的核心是提供constraint_generator 参数。仅使用 Type::Tiny 组件的最惯用方法是:

    package Types::Thunking;
    use Types::TypeTiny -all;
    use Type::Library -base;
    use Type::Utils -all;
    declare "Thunk", constraint_generator => sub { union [ CodeLike, @_ ] };
    

    就是这样!如果没有给出参数,它就像CodeLike 一样工作。这些库可以处理任何“内联”代码生成。

    它如此短的原因是constraint_generator 必须返回either一个代码引用,这可能是一个捕获传递给它的参数的闭包(见下文),或只是一个Type::Tiny - 在这种情况下不需要other parameterisability parameters。由于union(看起来它通常用于为declare 生成参数)返回一个适当构造的Type::Tiny::Union,所以它完美地融入其中。

    更详细的版本,不使用联合类型(为简洁起见,使用CodeRef 而不是CodeLike

    package Types::Thunking;
    use Types::Standard -all;
    use Type::Library -base;
    use Type::Utils -all;
    declare "Thunk",
      constraint_generator => sub {
        my ($param) = @_;
        die "parameter must be a type" if grep !UNIVERSAL::isa($_, 'Type::Tiny'), @_;
        return sub { is_CodeRef($_) or $param->check($_) };
      },
      inline_generator => sub {
        my ($param) = @_;
        die "parameter must be a type" if grep !UNIVERSAL::isa($_, 'Type::Tiny'), @_;
        return sub {
          my ($constraint, $varname) = @_;
          return sprintf(
            'Types::Standard::is_CodeRef(%s) or %s',
            $varname,
            $param->inline_check($varname),
          );
        };
      };
    

    这是我用来测试这些的“线束”:

    #!/usr/bin/perl
    use Thunking;
    sub do_test {
      use Data::Dumper; local $Data::Dumper::Terse = 1; local $Data::Dumper::Indent = 0;
      my ($args, $should_work) = @_;
      my $l = eval { Thunking->new(@$args) };
      if (!$l) {
        say "correctly did not work" and return if !$should_work;
        say "INcorrectly did not work" and return if $should_work;
      }
      my $val = eval { $l->attr };
      if (!$val) {
        say "correctly did not work" and return if !$should_work;
        say "INcorrectly did not work" and return if $should_work;
      }
      say(($should_work ? "" : "INcorrectly worked: "), Dumper $val);
    }
    do_test [attr => { k => "wrong type" }], 0;
    do_test [attr => ["real value at init"]], 1;
    do_test [attr => sub { [ "delayed" ] }], 1;
    do_test [attr => sub { { k => "delayed wrong type" } }], 0;
    

    延迟计算不可变属性

    为了使其不可变,我们希望将属性设置为失败,除非是我们自己做的。在读取属性时,我们想看看是否有计算要完成;如果是,就去做;然后返回值。

    天真的方法

    package Thunking;
    use Moo;
    use Types::Standard -all;
    use Types::Thunking -all;
    has attr  => (
      is => 'rwp',
      isa => Thunk[ArrayRef],
      required => 1,
    );
    before 'attr' => sub {
      my $self = shift;
      return if @_; # attempt at setting, hand to auto
      my $value = $self->{attr};
      return if ref($value) ne 'CODE'; # attempt at reading and already resolved
      $self->_set_attr($value->());
    }
    

    before 应该是不言自明的,但您会看到它手动查看对象的 hash-ref,这通常表明您的编程尚未完成。另外,它是rwp,并且需要类中的before,这远非漂亮。

    使用MooX 模块

    一种尝试使用单独的模块MooX::Thunking 来概括这一点的方法。首先,另一个封装Moo函数覆盖的模块:

    package MooX::Utils;
    use strict;
    use warnings;
    use Moo ();
    use Moo::Role ();
    use Carp qw(croak);
    use base qw(Exporter);
    our @EXPORT = qw(override_function);
    sub override_function {
      my ($target, $name, $func) = @_;
      my $orig = $target->can($name) or croak "Override '$target\::$name': not found";
      my $install_tracked = Moo::Role->is_role($target) ? \&Moo::Role::_install_tracked : \&Moo::_install_tracked;
      $install_tracked->($target, $name, sub { $func->($orig, @_) });
    }
    

    现在是 thunking MooX 模块本身,它使用上面的内容覆盖 has

    package MooX::Thunking;
    use MooX::Utils;
    use Types::TypeTiny -all;
    use Class::Method::Modifiers qw(install_modifier);
    sub import {
      my $target = scalar caller;
      override_function($target, 'has', sub {
        my ($orig, $name, %opts) = @_;
        $orig->($name, %opts), return if $opts{is} ne 'thunked';
        $opts{is} = 'ro';
        $orig->($name, %opts); # so we have method to modify
        install_modifier $target, 'before', $name => sub {
          my $self = shift;
          return if @_; # attempt at setting, hand to auto
          my $value = $self->{$name};
          return if !eval { CodeLike->($value); 1 }; # attempt at reading and already resolved
          $self->{$name} = $value->();
          $opts{isa}->($self->{$name}) if $opts{isa}; # validate
        }
      });
    }
    

    这会将“thunking”应用于属性。只有当属性为ro 时它才会起作用,并且会在读取时悄悄地解析任何CodeLike 值。可以这样使用:

    package Thunking;
    use Moo;
    use MooX::Thunking;
    use Types::Standard -all;
    use Types::Thunking -all;
    has attr => (
      is => 'thunked',
      isa => Thunk[ArrayRef],
    );
    

    使用BUILDARGSlazy

    强大的@haarg 建议的另一种方法:

    package MooX::Thunking;
    use MooX::Utils;
    use Types::TypeTiny -all;
    use Class::Method::Modifiers qw(install_modifier);
    sub import {
      my $target = scalar caller;
      override_function($target, 'has', sub {
        my ($orig, $name, %opts) = @_;
        $orig->($name, %opts), return if $opts{is} ne 'thunked';
        $opts{is} = 'lazy';
        my $gen_attr = "_gen_$name";
        $orig->($gen_attr => (is => 'ro'));
        $opts{builder} = sub { $_[0]->$gen_attr->(); };
        install_modifier $target, 'around', 'BUILDARGS' => sub {
          my ($orig, $self) = (shift, shift);
          my $args = $self->$orig(@_);
          $args->{$gen_attr} = delete $args->{$name} if eval { CodeLike->($args->{$name}); 1 };
          return $args;
        };
        $orig->($name, %opts);
      });
    }
    

    它使用内置的lazy 机制,创建一个builder,如果这是给定的,它将调用提供的CodeLike。一个重要的缺点是这种技术不适用于Moo::Roles。

    【讨论】:

    • 代码很多,没有任何解释。虽然很高兴您能分享您的解决方案,但它会大大受益于解释正在发生的事情、为什么会这样以及输出是什么的附加文本。
    • 对于它的价值,我根本没有投票。我认为您的回答还不错,只是目前的用处不大。
    • 很公平,这实际上很有帮助。更多解释在路上!
    猜你喜欢
    • 1970-01-01
    • 2016-08-22
    • 2013-12-15
    • 1970-01-01
    • 2016-09-02
    • 2014-11-25
    • 1970-01-01
    • 1970-01-01
    • 2010-12-28
    相关资源
    最近更新 更多