【问题标题】:Struggling with using pure functional programming to solve an everyday problem努力使用纯函数式编程来解决日常问题
【发布时间】:2011-09-05 15:40:07
【问题描述】:

我今天在hacker news 中看到了this post。我正在努力解决同样的问题,即理解纯函数式编程将如何帮助我抽象出一个现实世界的问题。 7 年前,我从命令式编程转向了面向对象编程。我觉得我已经掌握了它,它对我很有帮助。在过去的几年里,我学到了一些函数式编程的技巧和概念,比如 map 和 reduce,我也喜欢它们。我已经在我的 OO 代码中使用了它们,并且对此很满意,但是在抽象一组指令时,我只能想到 OO 抽象来使代码更漂亮。

最近我一直在研究python中的一个问题,我一直在努力避免使用OO来解决它。在大多数情况下,我的解决方案看起来势在必行,而且我知道如果我使用 OO,我可以让它看起来既漂亮又干净。我想我会发布这个问题,也许功能专家可以提出一个既美观又实用的解决方案。如果必须,我可以发布我丑陋的代码,但宁愿不要。 :) 问题来了:

用户可以请求图像或图像的缩略图。如果用户请求图像的缩略图,但它还不存在,请使用 python 的 PIL 模块创建它。还要使用人类可读的路径创建指向原始图像或缩略图的符号链接,因为原始图像名称是哈希码,而不是对其内容的描述。最后,重定向到该图像的符号链接。

在 OO 中,我可能会创建一个 SymlinkImage 基类、一个 ThumbnailSymlinkImage 子类和一个 OriginalSymlinkImage 子类。共享数据(在 SymlinkImage 类中)将类似于原始路径。共享行为将创建符号链接。子类将实现一个名为“generate”之类的方法,如果适用,该方法将负责创建缩略图,并调用其超类以创建新的符号链接。

【问题讨论】:

标签: functional-programming paradigms


【解决方案1】:

是的,您确实会使用函数式方法以非常不同的方式做到这一点。

这是一个使用类型化、默认纯函数式编程语言Haskell 的草图。我们为您的问题的关键概念创建新类型,并将工作分解为一次执行一项任务的离散函数。 IO 和其他副作用(如创建符号链接)仅限于某些功能,并用类型指示。为了区分这两种操作模式,我们使用a sum type

--
-- User can request an image or a thumbnail of the image.
-- If the user requests the thumbnail of the image, and it doesn't yet exist, create it using
-- python's PIL module. Also create a symbolic link to the original or
-- thumbnail with a human readable path, because the original image name is a
-- hashcode, and not descriptive of it's contents. Finally, redirect to the
-- symbolic link of that image.
--

module ImageEvent where

import System.FilePath
import System.Posix.Files

-- Request types
data ImgRequest = Thumb ImgName | Full ImgName

-- Hash of image 
type ImgName = String

-- Type of redirects
data Redirect

request :: ImgRequest -> IO Redirect
request (Thumb img) = do
    f <- createThumbnail img
    let f' = normalizePath f
    createSymbolicLink f f'
    return (urlOf f)

request (Full img)  = do
    createSymbolicLink f f'
    return (urlOf f)
    where
        f  = lookupPath img
        f' = normalizePath f

还有一些助手,我将由你来定义。

-- Creates a thumbnail for a given image at a path, returns new filepath
createThumbnail :: ImgName -> IO FilePath
createThumbnail f = undefined
    where
        p = lookupPath f

-- Create absolute path from image hash
lookupPath :: ImgName -> FilePath
lookupPath f = "/path/to/img" </> f <.> "png"

-- Given an image, construct a redirect to that image url
urlOf :: FilePath -> Redirect
urlOf = undefined

-- Compute human-readable path from has
normalizePath :: FilePath -> FilePath
normalizePath = undefined

一个真正漂亮的解决方案是用一个数据结构抽象出请求/响应模型来表示要执行的命令序列。一个请求进来,构建一个结构纯粹是为了代表它需要完成的工作,然后交给执行引擎来完成诸如创建文件之类的事情。那么核心逻辑将完全是纯函数(不是这个问题有很多核心逻辑)。对于这种带有效果的真正纯函数式编程风格的示例,我推荐 Wouter Swiestra 的论文,``Beauty in the Beast: A Functional Semantics for the Awkward Squad''

【讨论】:

  • 您采用的方法或多或少是程序性的;并不是说这是“坏”之类的。哦,谢谢你的链接!
【解决方案2】:

改变你的思维方式的唯一方法就是改变你的思维方式。我可以告诉你什么对我有用:

我想从事一个需要并发的个人项目。我环顾四周,找到了 erlang。我选择它是因为我认为它对并发的支持最好,而不是出于任何其他原因。我以前从未使用过函数式语言(为了比较,我在 1990 年代初开始进行面向对象编程。)

我读过 Armstrong 的 erlang 书。这很艰难。我有一个小项目要做,但我一直在努力。

该项目失败了,但几个月后,我已经在脑海中充分映射了所有内容,以至于我不再像以前那样思考对象了。

我确实经历了一个将对象映射到 erlang 进程的阶段,但这并没有太长,我摆脱了它。

现在切换范式就像切换语言,或者从一辆车换到另一辆车。开我父亲的车和我的卡车感觉不一样,但很快就又习惯了。

我认为使用 Python 工作可能会阻碍您,我强烈建议您查看 erlang。它的语法非常陌生——但这也很好,因为更传统的语法(至少对我而言)会导致尝试以旧方式对其进行编程并感到沮丧。

【讨论】:

  • 扩展erlang推荐:Erlang、Haskell、LISP、ML、F#等都是做函数式编程的好语言。像 C# / python / ruby​​ / js / php 这样的混合体给你太多的机会来回退到命令式技术
【解决方案3】:

就个人而言,我认为问题在于您正在尝试使用函数式编程来解决为命令式编程设计/陈述的问题。 3 种流行的范式(函数式、命令式、面向对象)具有不同的优势:

  • 函数式编程强调描述要做什么,通常是输入/结果。
  • 命令式编程强调如何做某事,通常根据要采取的步骤的列表和顺序,以及要修改的状态。
  • 面向对象的编程强调系统中实体之间的关系

因此,当您处理一个问题时,首要任务是重新表述它,以便预期的范式可以正确解决它。顺便说一句,据我所知,作为一个侧节点,没有“纯OOP”之类的东西。 OOP 类(无论是 Java、C#、C++、Python 还是 Objective C)的方法中的代码都是命令式的。

回到你的例子:你陈述问题的方式(首先,然后,最后)本质上是必不可少的。因此,构建功能解决方案几乎是不可能的(也就是说,不使用副作用或 monad 之类的技巧)。同样,即使您创建了一堆类,这些类本身也是无用的。要使用它们,您必须编写逐步解决问题的命令式代码(尽管这些代码嵌入在类中)。

重申问题:

  • 输入:图像类型(完整或缩略图)、图像名称、文件系统
  • 输出:请求的图像,具有请求图像的文件系统

从新的问题陈述中,你可以这样解决:

def requestImage(type, name, fs) : 
    if type == "full" :
        return lookupImage(name, fs), fs
    else:
        thumb = lookupThumb(name, fs)
        if(thumb) :
            return thumb, fs
        else:
            thumb = createThumbnail(lookupImage(name, fs))
            return thumb, addThumbnailToFs(fs, name, thumb)

当然,这是不完整的,但我们总能以大致相同的方式递归求解lookupImage、lookupThumb、createThumbnail和addThumbnailToFs。

重要提示:创建一个新的文件系统听起来很大,但它不应该是。例如,如果这是一个更大的 web 服务器中的一个模块,“新文件系统”可以像新缩略图应该在哪里的指令一样简单。或者,在最坏的情况下,将缩略图放置到适当的位置可能是一个 IO monad。

【讨论】:

  • “几乎不可能构建一个功能性解决方案”——我只是直接写了一个......将问题分解为其组成部分,类型的使用和关注点的强分离不是魔术或技巧。他们是工程师。
  • 我个人不认为副作用(在 ML、LISP 家族和 Erlang 中)和 Monad(在 Haskell 中)是真正的函数式编程。前者很明显;后者或多或少是要执行的(状态改变)操作列表,因此并不是真正的结果组合。
  • 对我来说,从数据转换的角度来考虑函数式编程是有帮助的。我发现我使用 FP 的方式与我过去使用 perl 的方式一样,早在我从粉红色的骆驼书中学习到它时。
  • @magice "Monads 不是真正的编程" Monads 是你对 OO 的替代品。 FP的很大一部分。还有很多函数式和面向对象的语言。 OO 与函数式/命令式拆分无关,它是正交的。仅仅因为你有 OO / 类并不意味着你必须以命令的方式使用它们。
  • +1 @Raynos。有一个* thread 详细讨论了OO-FP 错误二分法。