Pondering name of pattern seen in Elm and if other

2019-06-20 15:03发布

问题:

I'm currently a student of FP. As I look around different syntax offer by different functional languages, I've come across a pattern in Elm sample code. I am curious about it.

Here is the sample code

myList = [{foo = "bar1"},{foo = "bar2"}]    
foos = myList |> List.map .foo

In the last line here, List.map is being passed .foo. I believe this style is called point-free but what about the specific pattern of passing in an attribute to the List.map function?

Is this a more common thing? Is it possible to do this in Haskell? F#? Scala? Thanks for any help.

What is the (or is there a) formal (or informal ?) name for the pattern here? The property of an object is used as a short hand for a function that takes an object and calls said property on it?

回答1:

This is actually not point free, but rather syntactic sugar and the pipe forward operator. For point free see this article.

This can be written in fsharp as follows:

let foos = myList |> List.map (fun x -> x.foo)

And you can see immediately that this is equivalent to

List.map (fun x -> x.foo) myList

So the pipe operator just flips the arguments and makes it easy to chain operations together. So you pass your function and a list to the map. And the syntactic sugar in Elm allows you to skip the function parameter, by just writing out .foo. I think that feature is quite handy, btw.

Point-free would be when you avoid specifying the parameters of the function. It's typical FP but can be difficult to read once it gets complicated.

An example:

let mySum x y = x + y
//val mySum : x:int -> y:int -> int
mySum 4 7 //11

This is point free:

let mySum2 = (+)
//val mySum2 : (int -> int -> int)
mySum2 4 7 //11  


回答2:

If you think of your list as a "dataset" or "table", and consider each element in the list to be a "row", and the definition of the datatype of the elements as an enumeration of "attributes", then what you get is a kind of "projection" in the sense of relational algebra: https://en.wikipedia.org/wiki/Projection_(relational_algebra) .

Here is a Scala-example, which feels somewhat SQL-ish:

case class Row(id: Int, name: String, surname: String, age: Int)

val data = List(
  Row(0, "Bob", "Smith", 25),
  Row(1, "Charles", "Miller", 35),
  Row(2, "Drew", "Shephard", 45),
  Row(3, "Evan", "Bishop", 55)
)

val surnames = data map (_.surname)
val ages = data map (_.age)
val selectIdName = data map { row => (row.id, row.name) }

println(surnames)
// List(Smith, Miller, Shephard, Bishop)

println(selectIdName)
// List((0,Bob), (1,Charles), (2,Drew), (3,Evan))

Here, _.fieldName is a short syntax for an inline function literal of type Row => TypeOfTheField.

In Haskell, it's kind of trivial, because the declaration of a record datatype automatically brings all the getter functions into scope:

data Row = Row { id :: Int
               , name :: String
               , surname :: String
               , age :: Int
               } deriving Show

main = let dataset = [ Row 0 "Bob" "Smith" 25
                     , Row 1 "Charles" "Miller" 35
                     , Row 2 "Drew" "Shephard" 45
                     , Row 3 "Evan" "Bishop" 55
                     ]
       in print $ map name dataset
-- prints ["Bob","Charles","Drew","Evan"]

Even Java has something similar since version 8:

import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;

class JavaProjectionExample {
  private static class Row {
    private final int id;
    private final String name;
    private final String surname;
    private final int age;
    public Row(int id, String name, String surname, int age) {
      super();
      this.id = id;
      this.name = name;
      this.surname = surname;
      this.age = age;
    }
    public int getId() {
      return this.id;
    }
    public String getName() {
      return this.name;
    }
    public String getSurname() {
      return this.surname;
    }
    public int getAge() {
      return this.age;
    }
  }

  public static void main(String[] args) {
    List<Row> data = Arrays.asList(
      new Row(0, "Bob", "Smith", 25),
      new Row(1, "Charles", "Miller", 35),
      new Row(2, "Drew", "Shephard", 45),
      new Row(3, "Evan", "Bishop", 55)
    );

    List<Integer> ids = data.stream().map(Row::getId).collect(toList());
    List<String> names = data.stream().map(Row::getName).collect(toList());

    System.out.println(ids);
    System.out.println(names);
  }
}

Here, Row::getterName is a special syntax for getter methods, it is a value of type Function<Row, FieldType>.