Struggling with using pure functional programming to solve an everyday problem

I saw this post in hacker news today. I am struggling with the same problems of understanding how pure functional programming will help me abstract a real world problem. I made the switch from imperative to OO programming 7 years ago. I feel that I have mastered it, and it has served me well. In the last couple years I have learned some tricks and concepts in functional programming like map and reduce, and I like them as well. I have used them in my OO code, and have been happy with that, but when abstracting a set of instructions, I can only think of OO abstractions to make the code prettier.

Recently I have been working on a problem in python, and I have been trying to avoid using OO to solve it. For the most part my solution looks imperative, and I know that I could make it look nice and clean if I used OO. I thought I would post the problem, and maybe the functional experts can pose a solution that's beautiful and functional. I can post my ugly code if I must, but would rather not. :) Here's the problem:

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.

In OO I would likely create a SymlinkImage base class, a ThumbnailSymlinkImage subclass, and an OriginalSymlinkImage subclass. The shared data (in SymlinkImage class) will be things like the path to the original. The shared behavior will be creating the symbolic link. The subclasses will implement a method called something like 'generate' that will be responsible for creating the thumbnail if applicable, and making the call to their superclass to create the new symbolic link.


Yeah, you'd really do this very differently using a functional approach.

Here's a sketch using the typed, by-default pure, functional programming language Haskell. We create new types for the key concepts of your problem, and break apart the work into discrete functions that do one task at a time. The IO and other side effects (like creating a symlink) are restricted to certain functions, and indicated with a type. To distinguish the two modes of operation, we use 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

Along with some helpers, which I'll leave the definition up to you.

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

A truly beautiful solution would abstract out the request/response model with a data structure to represent the sequence of commands to be executed. A request comes in, builds a structure purely to represent what work it needs done, and that is handed to the execution engine with does things like creating files and so on. Then the core logic will be entirely pure functions (not that there's much core logic in this problem). For an example of this style of truly purely functional programming with effects, I recommend Wouter Swiestra's paper, ``Beauty in the Beast: A Functional Semantics for the Awkward Squad''


The only way to change your way of thinking is to change your way of thinking. I can tell you what worked for me:

I wanted to work on a personal project that required concurrency. I looked around and found erlang. I chose it because I thought it had the best support for concurrency, not for any other reason. I had never worked with a functional language before (and just for comparison, I started doing object oriented programming in the early 1990s.)

I read Armstrong's erlang book. It was tough. I had a small project to work on, but I just kept hammering at it.

The project was a failure, but after a couple months I had mapped everything in my head sufficiently that I no longer think in objects the way I used to.

I did go thru a phase where I mapped objects to erlang processes, but that wasn't too long and I got out of it.

Now switching paradigms is like switching languages, or going from one car to another. Driving my father's car feels different than my truck, but it doesn't take long to get used to it again.

I think working in Python might be holding you back, and I would strongly recommend you check out erlang. Its syntax is very alien-- but that's also good, as a more traditional syntax would (at least for me) lead to trying to program it the old ways and being frustrated.


Personally, I think the problem is that you are trying to use Functional Programming to solve problems that are designed/stated for Imperative programming. The 3 popular paradigms (Functional, Imperative, Object-oriented) have different strengths:

  • Functional Programming emphasizes on description of WHAT to be done, usually in term of input/result.
  • Imperative Programming emphasizes on HOW to do something, usually in term of list and order of steps to taken, and states to modify.
  • Object-oriented Programming emphasizes on the RELATIONSHIPS between entities in a system
  • Thus, when you approach a problem, the first order of business is to rephrase it such that the intended paradigm can properly solve it. By the way, as a side node, there is no such thing as "pure OOP" as far as I know. Code in the methods of your OOP classes (be it Java, C#, C++, Python, or Objective C) are all Imperative.

    Back to your example: the way that you state your problem (first, then, also, finally) is imperative in nature. As such, construction of a functional solution is almost impossible (without doing the tricks like side-effect or monads, that is). Similarly, even if you create a bunch of classes, those classes are useless in and of themselves. To use them, you have to write imperative code (albeit these codes are embedded within classes) that solve the problem step by step.

    To restate the problem:

  • Input: Image type (Full or thumbnail), Image name, file system
  • Output: the requested image, the file system with the requested image
  • From the new problem statement, you can solve it like this:

    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)
    

    Of course, this is incomplete, but we can always recursively solve lookupImage, lookupThumb, createThumbnail, and addThumbnailToFs roughly the same way.

    Big Note: creating a new filesystem sounds big, but it should not be. For example, if this is a module in a bigger web server, the "new filesystem" can be as simple as the instruction to where the new thumbnail should be. Or, at the very worst, it can be an IO monad to put the thumbnail to the appropriate location.

    链接地址: http://www.djcxy.com/p/42870.html

    上一篇: 如何构建一个Haskell项目?

    下一篇: 努力使用纯函数式编程来解决日常问题