What's the difference between abstraction and generalization?

后端 未结 8 1167
滥情空心
滥情空心 2021-01-30 13:26

I understand that abstraction is about taking something more concrete and making it more abstract. That something may be either a data structure or a procedure. For example:

8条回答
  •  天涯浪人
    2021-01-30 14:09

    I'd like to offer an answer for the greatest possible audience, hence I use the Lingua Franca of the web, Javascript.

    Let's start with an ordinary piece of imperative code:

    // some data
    
    const xs = [1,2,3];
    
    // ugly global state
    
    const acc = [];
    
    // apply the algorithm to the data
    
    for (let i = 0; i < xs.length; i++) {
      acc[i] = xs[i] * xs[i];
    }
    
    console.log(acc); // yields [1, 4, 9]

    In the next step I introduce the most important abstraction in programming - functions. Functions abstract over expressions:

    // API
    
    const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
    const concat = xs => ys => xs.concat(ys);
    const sqr_ = x => [x * x]; // weird square function to keep the example simple
    
    // some data
    
    const xs = [1,2,3];
    
    // applying
    
    console.log(
      foldr(x => acc => concat(sqr_(x)) (acc)) ([]) (xs) // [1, 4, 9]
    )

    As you can see a lot of implementation details are abstracted away. Abstraction means the suppression of details.

    Another abstraction step...

    // API
    
    const comp = (f, g) => x => f(g(x));
    const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
    const concat = xs => ys => xs.concat(ys);
    const sqr_ = x => [x * x];
    
    // some data
    
    const xs = [1,2,3];
    
    // applying
    
    console.log(
      foldr(comp(concat, sqr_)) ([]) (xs) // [1, 4, 9]
    );

    And another one:

    // API
    
    const concatMap = f => foldr(comp(concat, f)) ([]);
    const comp = (f, g) => x => f(g(x));
    const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
    const concat = xs => ys => xs.concat(ys);
    const sqr_ = x => [x * x];
    
    // some data
    
    const xs = [1,2,3];
    
    // applying
    
    console.log(
      concatMap(sqr_) (xs) // [1, 4, 9]
    );

    The underlying principle should now be clear. I'm still dissatisfied with concatMap though, because it only works with Arrays. I want it to work with every data type that is foldable:

    // API
    
    const concatMap = foldr => f => foldr(comp(concat, f)) ([]);
    const concat = xs => ys => xs.concat(ys);
    const sqr_ = x => [x * x];
    const comp = (f, g) => x => f(g(x));
    
    // Array
    
    const xs = [1, 2, 3];
    
    const foldr = f => acc => xs => xs.reduceRight((acc, x) => f(x) (acc), acc);
    
    // Option (another foldable data type)
    
    const None =      r => f => r;
    const Some = x => r => f => f(x);
    
    const foldOption = f => acc => tx => tx(acc) (x => f(x) (acc));
    
    // applying
    
    console.log(
      concatMap(foldr) (sqr_) (xs), // [1, 4, 9]
      concatMap(foldOption) (sqr_) (Some(3)), // [9]
      concatMap(foldOption) (sqr_) (None) // []
    );

    I broadened the application of concatMap to encompass a larger domain of data types, nameley all foldable datatypes. Generalization emphasizes the commonalities between different types, (or rather objects, entities).

    I achieved this by means of dictionary passing (concatMap's additional argument in my example). Now it is somewhat annoying to pass these type dicts around throughout your code. Hence the Haskell folks introduced type classes to, ...um, abstract over type dicts:

    concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
    
    concatMap (\x -> [x * x]) ([1,2,3]) -- yields [1, 4, 9]
    concatMap (\x -> [x * x]) (Just 3) -- yields [9]
    concatMap (\x -> [x * x]) (Nothing) -- yields []
    

    So Haskell's generic concatMap benefits from both, abstraction and generalization.

提交回复
热议问题