【问题标题】:Lazy evaluation container for dynamic programming?动态编程的惰性评估容器?
【发布时间】:2013-10-27 17:09:56
【问题描述】:

我有一些对我很有效的模式,但是我很难向其他程序员解释。我正在寻找一些理由或文献参考。

我个人使用 PHP,但这也适用于 Java、Javascript、C++ 和类似语言。示例将在 PHP 或 Pseudocode 中,我希望你能忍受。

这个想法是对中间结果使用惰性求值容器,以避免对同一个中间值进行多次计算。

“动态编程”:

http://en.wikipedia.org/wiki/Dynamic_programming

动态规划方法试图只解决每个子问题一次,从而减少计算次数:一旦计算了给定子问题的解决方案,它就会被存储或“记忆化”:下一次相同的解决方案是需要的,简单查一下

惰性评估容器:

class LazyEvaluationContainer {

  protected $values = array();

  function get($key) {
    if (isset($this->values[$key])) {
      return $this->values[$key];
    }
    if (method_exists($this, $key)) {
      return $this->values[$key] = $this->$key();
    }
    throw new Exception("Key $key not supported.");
  }

  protected function foo() {
    // Make sure that bar() runs only once.
    return $this->get('bar') + $this->get('bar');
  }

  protected function bar() {
    .. // expensive computation.
  }
}

使用类似的容器,例如作为依赖注入容器(DIC)。

详情

我通常使用这个的一些变体。

  • 是否可以将实际数据方法与数据计算方法放在不同的对象中?
  • 是否可以有带参数的计算方法,使用带有嵌套数组的缓存?
  • 在 PHP 中,可以使用魔术方法(__get() 或 __call())作为主要检索方法。结合类 docblock 中的“@property”,可以为每个“虚拟”属性提供类型提示。
  • 我经常使用诸如“get_someValue()”之类的方法名称,其中“someValue”是实际的键,以区别于常规方法。
  • 是否可以将数据计算分配给多个对象,以获得某种关注点分离?
  • 可以预初始化一些值吗?

编辑:问题

关于 Spring @Configuration 类中的一个可爱的机制,已经有一个很好的答案。

为了使这个更有用和有趣,我稍微扩展/澄清这个问题:

  • 存储来自动态编程的中间值是否合法?
  • 在 PHP 中实现此功能的最佳实践是什么? “细节”中的一些东西是不是又丑又丑?

【问题讨论】:

  • 所以...它是一个缓存,不是吗?
  • 您正在缓存结果?
  • 是的,这是一种说法。当然缓存在内存级别,没有持久存储。
  • 顺便说一句,我来自 Composer 的这个 PR,github.com/composer/composer/pull/2298。不过,我不希望任何不真正感兴趣的人阅读这篇文章。 (是的,我意识到 igorw 抱怨构造函数是有道理的)

标签: php dynamic-programming lazy-evaluation data-containers


【解决方案1】:

如果我理解正确,这是一个相当标准的过程,尽管正如你正确承认的那样,它与 DI(或引导应用程序)相关。

一个具体的、规范的例子是任何带有惰性 bean 定义的 Spring @Configuration 类;我认为它显示的行为与您描述的完全相同,尽管完成它的实际代码隐藏在视图之外(并在幕后生成)。实际的 Java 代码可能是这样的:

@Configuration
public class Whatever {

  @Bean @Lazy
  public OneThing createOneThing() {
    return new OneThing();
  }

  @Bean @Lazy
  public SomeOtherThing createSomeOtherThing() {
    return new SomeOtherThing();
  }

  // here the magic begins:

  @Bean @Lazy
  public SomeThirdThing getSomeThirdThing() {
    return new SomeThirdThing(this.createOneThing(), this.createOneThing(), this.createOneThing(), createSomeOtherThing());
  }
}

每个标有@Bean @Lazy 的方法都代表一个“资源”,一旦需要(并调用该方法)就会创建该“资源”,并且 - 无论该方法被调用多少次 - 对象只会被创建一次(由于在加载过程中改变了实际代码的一些魔法)。因此,即使在createOneThing() 中似乎在createOneThing() 中调用了两次,也只会发生一次调用(这只是在有人尝试调用createSomeThirdThing() 或在ApplicationContext 上调用getBean(SomeThirdThing.class) 之后)。

【讨论】:

  • 所以“@Lazy”是一个被 Spring 拾取并编译成一些花哨机制的注解?不错:)
  • (我考虑将此标记为接受+赏金,但想等一下其他人的想法)
  • 还有一个问题:在 Java/Spring 中,您是否会或可以将其用于动态编程,其中您有值而不是对象?那些@Lazy 东西可以带参数,每个参数值缓存吗?
  • 哦,谢谢 :-) 抱歉,我没有注意到您关于 Java/Spring 的问题。 1)在Java中,除了对象之外没有“值”(字符串也是对象,整数和字符不被认为是有趣的)。 @Lazy 只是对@Bean 的补充,它不是用来存储计算结果的,而是用来引导应用程序的(有点像 Zend_Application_Bootstrap,它意外地有类似的机制来加载依赖资源)。
  • 过早授予奖金的坏处在于,那些已经阅读了已宣布的到期日期的人现在为时已晚。您知道在所有答案都存在之后,您有一个宽限期来授予它吗?
【解决方案2】:

我认为你不可能对所有东西都有一个通用的惰性评估容器。

让我们首先讨论一下你真正拥有的东西。我不认为这是懒惰的评价。延迟评估被定义为将评估延迟到真正需要该值的点,并与对该值的进一步请求共享已评估的值。

我想到的典型例子是数据库连接。您需要准备好一切,以便在需要时使用该连接,但只有在确实需要数据库查询时,才会创建连接,然后与后续查询共享。

典型的实现是将连接字符串传递给构造函数,在内部存储,当调用查询方法时,首先调用返回连接句柄的方法,该方法将创建并保存该句柄如果它不存在,则使用连接字符串。以后对该对象的调用将重用现有的连接。

这样的数据库对象有资格对数据库连接进行延迟评估:它仅在真正需要时创建,然后为所有其他查询共享。

当我查看您的实现时,它不符合“仅在真正需要时进行评估”的条件,它只会存储曾经创建的值。所以它实际上只是某种缓存。

它也没有真正解决普遍只在全局范围内评估昂贵计算的问题。如果您有两个实例,您将运行两次昂贵的函数。但另一方面,不对其进行两次评估会引入全局状态——除非明确声明,否则这应该被认为是一件坏事。通常它会使代码很难正确测试。我个人会避免这种情况。

是否可以将实际数据方法与数据计算方法放在不同的对象中?

如果您查看 Zend 框架如何提供缓存模式 (\Zend\Cache\Pattern\{Callback,Class,Object}Cache),您会发现真正的工作类正在使用装饰器包裹它。获取存储的值并读回它们的所有内部工作都是在内部处理的,您可以像以前一样从外部调用您的方法。

缺点是您没有原始类类型的对象。因此,如果您使用类型提示,则不能传递修饰的缓存对象而不是原始对象。解决方案是实现一个接口。原始类用真正的功能实现它,然后你创建另一个类来扩展缓存装饰器并实现接口。该对象将通过类型提示检查,但您必须手动实现所有接口方法,这些方法无非是将调用传递给原本会拦截它们的内部魔术函数。

interface Foo
{
    public function foo();
}

class FooExpensive implements Foo
{
    public function foo()
    {
        sleep(100);
        return "bar";
    }
}

class FooCached extends \Zend\Cache\Pattern\ObjectPattern implements Foo
{
    public function foo()
    {
        //internally uses instance of FooExpensive to calculate once
        $args = func_get_args();
        return $this->call(__FUNCTION__, $args); 
    }
}

我发现如果没有这两个类和一个接口,在 PHP 中实现缓存是不可能的(但另一方面,针对接口实现是一件好事,它不应该打扰你)。您不能简单地直接使用本机缓存对象。

是否可以有带参数的计算方法,使用带有嵌套数组的缓存?

参数在上述实现中起作用,它们用于缓存键的内部生成。你应该看看\Zend\Cache\Pattern\CallbackCache::generateCallbackKey 方法。

在 PHP 中,可以使用魔术方法(__get() 或 __call())作为主要检索方法。结合类文档块中的“@property”,这允许为每个“虚拟”属性提供类型提示。

魔法是邪恶的。文档块应该被认为是过时的,因为它不是真正的工作代码。虽然我发现在一个非常易于理解的值对象代码中使用魔法 getter 和 setter 是可以接受的,这将允许在 any 属性中存储 any 值,就像 @ 987654325@,我建议对__call 非常小心。

我经常使用“get_someValue()”之类的方法名称,其中“someValue”是实际的键,以区别于常规方法。

我认为这违反了 PSR-1:“4.3. 方法:方法名称必须在 camelCase() 中声明。”是否有理由将这些方法标记为特殊的?它们很特别吗?确实返回值,不是吗?

是否可以将数据计算分配给多个对象,以获得某种关注点分离?

如果你缓存一个复杂的对象构造,这是完全可能的。

可以预初始化一些值吗?

这不应该是缓存的问题,而是实现本身的问题。不执行昂贵的计算,而是返回预设值有什么意义?如果这是一个真实的用例(例如,如果参数超出定义的范围,则立即返回 NULL),它必须是实现本身的一部分。在这种情况下,您不应依赖对象周围的附加层来返回值。

存储来自动态编程的中间值是否合法?

您有动态规划问题吗?您链接的维基百科页面上有这句话:

要使动态规划适用,问题必须具有两个关键属性:最优子结构和重叠子问题。如果一个问题可以通过组合非重叠子问题的最优解来解决,则该策略称为“分而治之”。

我认为已经存在一些模式似乎可以解决您的示例中的惰性评估部分:Singleton、ServiceLocator、Factory。 (我不是在这里宣传单身人士!)

还有“承诺”的概念:返回的对象承诺稍后会在被询问时返回实际值,但只要现在不需要该值,就会充当可以传递的值替换相反。您可能想阅读这篇博文:http://blog.ircmaxell.com/2013/01/promise-for-clean-code.html

在 PHP 中实现这一点的最佳实践是什么? “细节”中的一些东西是不是又丑又丑?

您使用的示例可能接近斐波那契示例。我不喜欢该示例的方面是您使用单个实例来收集所有值。在某种程度上,你在这里聚合了全局状态——这可能就是整个概念的意义所在。但是全局状态是邪恶的,我不喜欢那个额外的层。而且你还没有真正解决参数的问题。

我想知道为什么在foo() 中真的有两个对bar() 的调用?更明显的方法是将结果直接复制到foo(),然后“添加”它。

总而言之,直到现在我还没有印象深刻。在这个简单的层面上,我无法预料到这样一个通用解决方案的真实用例。我喜欢 IDE 自动建议支持,我不喜欢鸭式打字(传递一个只模拟兼容但无法确保实例的对象)。

【讨论】:

  • 现在这是一个有用的回复!可惜赏金已经用完了。
  • 对我的用例过于详尽:确实,它可能无法满足您提到的重叠子问题的动态编程标准。到目前为止,我在不同的情况下使用了这个“模式”,但它们都是关于一个复杂的问题,需要分成不同的中间步骤。这些中间结果很少是动态的,大多数中间结果都可以用没有参数的单一方法确定。例如。如果你组装一头大象,那么你的中间结果可以是器官、耳朵、腿。但每一个都是独一无二的,最终结果也是如此。
  • 一些子问题可能会重叠,这给了我们一些聪明的重用,但这只是一个很好的副作用。主要目的是避免疯狂的参数列表,取而代之的是具有通用键的中间值的封闭空间。因此,在这个封闭的空间内,“elephantNose”只有一个含义,并且随处可见。
  • 这与 DIC 用例完全相同 - 例如Symfony 2 的已编译 DIC,其中到处都是 $this->get($key),但您没有动态编程或分治法的典型示例的典型递归和参数化。跨度>
  • 我还没有看到那个杀手级的论点。构建对象应该是 DI 框架的任务。将“带有疯狂参数列表”的参数化方法转换为单个方法调用似乎有点奇怪。
猜你喜欢
  • 2023-04-03
  • 2014-02-08
  • 2013-03-11
  • 2017-01-18
  • 2018-11-11
  • 2015-05-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多