Java 8: TriFunction (và kin) trong java.util. Chức năng ở đâu? Hoặc thay thế là gì?


113

Tôi thấy java.util. Chức năng.BiFunction, vì vậy tôi có thể thực hiện việc này:

BiFunction<Integer, Integer, Integer> f = (x, y) -> { return 0; };

Nếu điều đó không đủ tốt và tôi cần TriFunction thì sao? Nó không tồn tại!

TriFunction<Integer, Integer, Integer, Integer> f = (x, y, z) -> { return 0; };

Tôi đoán tôi nên nói thêm rằng tôi biết tôi có thể xác định TriFunction của riêng mình, tôi chỉ đang cố gắng hiểu lý do đằng sau việc không đưa nó vào thư viện tiêu chuẩn.


1
với giao diện bifunction, bạn easly có thể định nghĩa lớp N-hàm, nếu bạn xác định trifunction như giao diện riêng biệt, sb đầu tiên sẽ được hỏi tại sao không quadofunction, và thứ hai, bạn cần phải lặp lại tất cả các phương pháp mà mất Bifunction như tham số
user902383

6
Có một điểm làm giảm lợi nhuận cho các API như thế này. (Cá nhân, tôi nghĩ rằng JDK8 thông qua nó một thời gian trở lại, nhưng điều này là ngoài thậm chí đó.)
Louis Wasserman

Tôi tin rằng lý do là để nói rằng Chức năng và Chức năng BiFunction đã được triển khai đầy đủ với các đối tượng và kiểu gốc. Bao gồm TriFunctions với tất cả các biến thể khác nhau sẽ làm nổ tung JRE với các lớp và phương thức.
Thorbjørn Ravn Andersen

1
Câu trả lời ngắn. Trong Java, nếu bạn không thấy nó, bạn tự xây dựng (tất nhiên là xem câu trả lời của Alex P). Sidenote, trong C #, những người triển khai dotnet đã cung cấp cho bạn những cái được soạn sẵn (tối đa 16 đối số), nhưng không có tên tiền tố ("Bi" ở đây): xem docs.microsoft.com/en-us/dotnet/api/… Chỉ là một "Func" đơn giản. Vì vậy, đây là một trong những nơi tôi thích dotnet hơn java. Xin đừng biến phần bình luận này thành một cuộc chiến h0ly. và chỉ giới hạn nhận xét cho BiFunction.
granadaCoder

Câu trả lời:


81

Theo như tôi biết, chỉ có hai loại chức năng, phá hủy và xây dựng.

Trong khi chức năng xây dựng, như tên của nó, tạo ra một cái gì đó, một chức năng phá hủy phá hủy một cái gì đó, nhưng không phải theo cách bạn có thể nghĩ bây giờ.

Ví dụ, hàm

Function<Integer,Integer> f = (x,y) -> x + y  

là một trong những xây dựng . Khi bạn cần xây dựng một cái gì đó. Trong ví dụ, bạn đã xây dựng bộ tuple (x, y) . Các hàm cấu tạo có vấn đề là không thể xử lý các đối số vô hạn. Nhưng điều tồi tệ nhất là, bạn không thể cứ để ngỏ một cuộc tranh cãi. Bạn không thể chỉ nói "well, let x: = 1" và thử mọi y bạn có thể muốn thử. Bạn phải xây dựng mỗi khi toàn bộ tuple với x := 1. Vì vậy, nếu bạn muốn xem những gì các hàm trả về cho y := 1, y := 2, y := 3bạn, bạn phải viết f(1,1) , f(1,2) , f(1,3).

Trong Java 8, các hàm xây dựng nên được xử lý (hầu hết thời gian) bằng cách sử dụng tham chiếu phương thức vì không có nhiều lợi ích khi sử dụng hàm lambda có tính xây dựng. Chúng hơi giống các phương thức tĩnh. Bạn có thể sử dụng chúng, nhưng chúng không có trạng thái thực.

Loại còn lại là loại phá hoại, nó lấy một thứ gì đó và tháo dỡ nó nếu cần. Ví dụ, hàm hủy diệt

Function<Integer, Function<Integer, Integer>> g = x -> (y -> x + y) 

làm tương tự như chức năng fđược xây dựng. Lợi ích của một hàm hủy là bạn có thể xử lý các đối số vô hạn ngay bây giờ, điều này đặc biệt thuận tiện cho các luồng và bạn chỉ có thể để mở các đối số. Vì vậy, nếu bạn lại muốn xem kết quả sẽ như thế nào nếu x := 1y := 1 , y := 2 , y := 3, bạn có thể nói h = g(1)h(1)là kết quả cho y := 1, h(2)cho y := 2h(3)cho y := 3.

Vì vậy, ở đây bạn có một trạng thái cố định! Điều đó khá năng động và đó là hầu hết thời gian những gì chúng tôi muốn từ một lambda.

Các mẫu như Factory sẽ dễ dàng hơn rất nhiều nếu bạn có thể chỉ cần đưa vào một chức năng làm việc cho bạn.

Những cái phá hủy dễ dàng kết hợp với nhau. Nếu loại phù hợp, bạn có thể chỉ cần soạn chúng theo ý muốn. Bằng cách sử dụng đó, bạn có thể dễ dàng xác định các hình thái giúp việc thử nghiệm (với các giá trị không thay đổi) dễ dàng hơn rất nhiều!

Bạn cũng có thể làm điều đó với một cấu trúc, nhưng bố cục phá hủy trông đẹp hơn và giống như một danh sách hoặc một trình trang trí, và một cấu trúc trông rất giống một cái cây. Và những thứ như backtracking với các chức năng xây dựng chỉ là không tốt. Bạn chỉ có thể lưu các hàm từng phần của hàm hủy (lập trình động) và trên "backtrack" chỉ cần sử dụng hàm hủy cũ. Điều đó làm cho mã nhỏ hơn rất nhiều và dễ đọc hơn. Với các hàm xây dựng, bạn ít nhiều phải nhớ tất cả các đối số, có thể là rất nhiều.

Vì vậy, tại sao cần BiFunctionphải có nhiều câu hỏi hơn là tại sao không có TriFunction?

Trước hết, rất nhiều thời gian bạn chỉ có một vài giá trị (nhỏ hơn 3) và chỉ cần một kết quả, vì vậy một hàm hủy thông thường sẽ không cần thiết, một hàm xây dựng sẽ tốt. Và có những thứ như monads thực sự cần một chức năng xây dựng. Nhưng ngoài điều đó ra, thực sự không có nhiều lý do chính đáng tại sao lại có BiFunction. Điều đó không có nghĩa là nó nên bị loại bỏ! Tôi chiến đấu cho Môn phái của mình cho đến khi chết!

Vì vậy, nếu bạn có nhiều đối số, mà bạn không thể kết hợp thành một lớp vùng chứa logic và nếu bạn cần hàm có tính xây dựng, hãy sử dụng tham chiếu phương thức. Nếu không, hãy thử sử dụng khả năng mới có được của các hàm phá hủy, bạn có thể thấy mình đang làm rất nhiều việc với ít dòng mã hơn rất nhiều.


2
Bạn đã trả lời câu hỏi của tôi ... Tôi nghĩ ... Tôi không biết liệu các nhà thiết kế ngôn ngữ java có xuất phát từ dòng suy nghĩ này hay không, nhưng tôi không rành về lập trình hàm. Cám ơn vì đã giải thích.
Richard Finegan

79
Tôi chưa bao giờ thấy các thuật ngữ mang tính xây dựngphá hoại được sử dụng để chỉ các khái niệm bạn mô tả. Tôi nghĩ rằng cà rikhông cà ri là những thuật ngữ phổ biến hơn.
Feuermurmel

17
Ví dụ về hàm đầu tiên không đúng về mặt cú pháp. Nó phải là BiFunction chứ không phải Hàm, vì nó cần hai đối số đầu vào.
báo

3
IMO BiFunctionđược tạo ra để cho phép giảm dữ liệu dễ dàng và hầu hết Streamcác hoạt động đầu cuối của chỉ là giảm dữ liệu. Một ví dụ điển hình là BinaryOperator<T>, được sử dụng trong nhiều Collectors. Phần tử đầu tiên bị giảm với phần tử thứ hai, sau đó có thể được giảm với phần tử tiếp theo, v.v. Tất nhiên, bạn có thể tạo một Function<T, Function<T, T>mã giảm func = x -> (y -> / * tại đây * /). Nhưng, nghiêm túc? Tất cả những điều này khi bạn có thể làm một cách đơn giản BinaryOperator<T> func = (x, y) -> /*reduction code here*/. Thêm vào đó, cách tiếp cận giảm dữ liệu này có vẻ rất giống với cách tiếp cận "phá hoại" của bạn đối với tôi.
FBB

32
Làm thế nào mà điều này lại nhận được nhiều ủng hộ như vậy? Đó là một câu trả lời khủng khiếp và khó hiểu, bởi vì nó dựa trên tiền đề Function<Integer,Integer> f = (x,y) -> x + ylà Java hợp lệ, nhưng không phải vậy. Đó phải là một BiFunction để bắt đầu!
wvdz

162

Nếu bạn cần TriFunction, chỉ cần làm điều này:

@FunctionalInterface
interface TriFunction<A,B,C,R> {

    R apply(A a, B b, C c);

    default <V> TriFunction<A, B, C, V> andThen(
                                Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (A a, B b, C c) -> after.apply(apply(a, b, c));
    }
}

Sau đây chương trình nhỏ cho thấy nó có thể được sử dụng như thế nào. Hãy nhớ rằng kiểu kết quả được chỉ định là tham số kiểu chung cuối cùng.

  public class Main {

    public static void main(String[] args) {
        BiFunction<Integer, Long, String> bi = (x,y) -> ""+x+","+y;
        TriFunction<Boolean, Integer, Long, String> tri = (x,y,z) -> ""+x+","+y+","+z;


        System.out.println(bi.apply(1, 2L)); //1,2
        System.out.println(tri.apply(false, 1, 2L)); //false,1,2

        tri = tri.andThen(s -> "["+s+"]");
        System.out.println(tri.apply(true,2,3L)); //[true,2,3]
    }
  }

Tôi đoán nếu có sử dụng thực tế cho TriFunction trong java.util.*hoặc java.lang.*nó đã được định nghĩa. Tuy nhiên, tôi sẽ không bao giờ vượt quá 22 đối số ;-) Ý tôi là, tất cả mã mới cho phép truyền trực tuyến các bộ sưu tập không bao giờ yêu cầu TriFunction như bất kỳ tham số phương thức nào. Vì vậy, nó đã không được bao gồm.

CẬP NHẬT

Để hoàn thiện và làm theo lời giải thích các hàm phá hủy trong một câu trả lời khác (liên quan đến cà ri), đây là cách TriFunction có thể được mô phỏng mà không cần giao diện bổ sung:

Function<Integer, Function<Integer, UnaryOperator<Integer>>> tri1 = a -> b -> c -> a + b + c;
System.out.println(tri1.apply(1).apply(2).apply(3)); //prints 6

Tất nhiên, có thể kết hợp các chức năng theo những cách khác, ví dụ:

BiFunction<Integer, Integer, UnaryOperator<Integer>> tri2 = (a, b) -> c -> a + b + c;
System.out.println(tri2.apply(1, 2).apply(3)); //prints 6
//partial function can be, of course, extracted this way
UnaryOperator partial = tri2.apply(1,2); //this is partial, eq to c -> 1 + 2 + c;
System.out.println(partial.apply(4)); //prints 7
System.out.println(partial.apply(5)); //prints 8

Mặc dù việc làm quen với bất kỳ ngôn ngữ nào hỗ trợ lập trình hàm ngoài lambdas là điều tự nhiên, nhưng Java không được xây dựng theo cách này và mặc dù có thể thực hiện được, nhưng mã này rất khó duy trì và đôi khi có thể đọc được. Tuy nhiên, nó rất hữu ích như một bài tập và đôi khi các hàm một phần có một vị trí thích hợp trong mã của bạn.


6
Cảm ơn vì giải pháp. Và có, chắc chắn có sử dụng cho BiFunction, TriFunction, ... Nếu không mọi người sẽ không tìm kiếm nó. Nó đoán toàn bộ điều lambda hiện tại là quá mới đối với Oracle và sẽ được mở rộng trong các phiên bản Java sau này. Hiện tại, nó là một bằng chứng về khái niệm.
Stefan Endrullis

Hy @Alex bạn có thể vui lòng xác định dòng sau. chuyện gì đang xảy ra ở đây default <V> TriFunction <A, B, C, V> andThen (Function <? super R,? expand V> after) {Objects.requireNonNull (after); return (A a, B b, C c) -> after.apply (áp dụng (a, b, c)); }
Muneeb Nasir

@MuneebNasir - nó cho phép bạn thực hiện thành phần chức năng: TriFunction<Integer,Integer,Integer,Integer> comp = (x,y,z) -> x + y + z; comp = comp.andThen(s -> s * 2); int result = comp.apply(1, 2, 3); //12Xem stackoverflow.com/questions/19834611/…
Alex Pakka

Đã thêm andThen()ví dụ sử dụng vào câu trả lời.
Alex Pakka

Không chỉ currying không thích ứng tốt với ngôn ngữ Java, mà còn, hãy sửa lỗi cho tôi nếu tôi sai, nhưng BiFunctionđược sử dụng trong StreamAPI để thực hiện giảm dữ liệu, trông rất giống với cách tiếp cận currying đối với tôi: bạn không bao giờ mất nhiều hơn hai và bạn có thể xử lý bất kỳ số lượng phần tử nào, mỗi lần giảm một phần tử (xem nhận xét của tôi về câu trả lời được chấp nhận, tôi rất vui khi biết nếu tôi sai khi xem theo cách đó).
FBB

13

Cách khác là thêm phần phụ thuộc bên dưới,

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Bây giờ, bạn có thể sử dụng Hàm Vavr, chẳng hạn như bên dưới tối đa 8 đối số,

3 đối số:

Function3<Integer, Integer, Integer, Integer> f = 
      (a, b, c) -> a + b + c;

5 đối số:

Function5<Integer, Integer, Integer, Integer, Integer, Integer> f = 
      (a, b, c, d, e) -> a + b + c + d + e;

2
Tôi đã định cập nhật câu trả lời của mình để đề cập đến vavr, nhưng bạn là người đầu tiên, vì vậy tôi đã ủng hộ. Nếu bạn đến thời điểm bạn cần một TriFunction, có nhiều khả năng bạn sẽ tốt hơn bằng cách sử dụng vavrthư viện - nó làm cho việc lập trình kiểu chức năng trong Java trở nên dễ dàng nhất có thể.
Alex Pakka

7

Tôi có gần như cùng một câu hỏi và một phần câu trả lời. Không chắc liệu câu trả lời mang tính xây dựng / giải cấu trúc có phải là điều mà các nhà thiết kế ngôn ngữ đã nghĩ đến hay không. Tôi nghĩ rằng có từ 3 trở lên N có các trường hợp sử dụng hợp lệ.

Tôi đến từ .NET. và trong .NET, bạn có Func và Action cho các hàm void. Vị ngữ và một số trường hợp đặc biệt khác cũng tồn tại. Xem: https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx

Tôi tự hỏi lý do tại sao các nhà thiết kế ngôn ngữ đã chọn Chức năng, Chức năng và không tiếp tục cho đến khi DecaExiFunction?

Câu trả lời cho phần thứ hai là tẩy xóa kiểu. Sau khi biên dịch không có sự khác biệt giữa Func và Func. Do đó, phần sau không biên dịch:

package eu.hanskruse.trackhacks.joepie;

public class Functions{

    @FunctionalInterface
    public interface Func<T1,T2,T3,R>{
        public R apply(T1 t1,T2 t2,T3 t3);
    }

    @FunctionalInterface
    public interface Func<T1,T2,T3,T4,R>{
        public R apply(T1 t1,T2 t2,T3 t3, T4 t4);
    }
}

Các chức năng bên trong được sử dụng để phá vỡ một vấn đề nhỏ khác. Eclipse nhấn mạnh rằng có cả hai lớp trong các tệp có tên Hàm trong cùng một thư mục ... Không chắc liệu đây có phải là vấn đề trình biên dịch ngày nay hay không. Nhưng tôi không thể chuyển lỗi trong Eclipse.

Hàm được sử dụng để ngăn chặn xung đột tên với loại Hàm java.

Vì vậy, nếu bạn muốn thêm Func từ 3 đến 16 đối số, bạn có thể làm hai việc.

  • Tạo TriFunc, TesseraFunc, PendeFunc, ... DecaExiFunc, v.v.
    • (Tôi nên sử dụng tiếng Hy Lạp hay tiếng Latinh?)
  • Sử dụng tên gói hoặc lớp để làm cho các tên khác nhau.

Ví dụ cho cách thứ hai:

 package eu.hanskruse.trackhacks.joepie.functions.tri;

        @FunctionalInterface
        public interface Func<T1,T2,T3,R>{
            public R apply(T1 t1,T2 t2,T3 t3);
        }

package eu.trackhacks.joepie.functions.tessera;

    @FunctionalInterface
    public interface Func<T1,T2,T3,T4,R>{
        public R apply(T1 t1,T2 t2,T3 t3, T4 t4);
    }

Cách tiếp cận tốt nhất sẽ là gì?

Trong các ví dụ trên, tôi không bao gồm các triển khai cho các phương thức andThen () và compile (). Nếu bạn thêm chúng, bạn phải thêm mỗi lần 16 quá tải: TriFunc phải có andthen () với 16 đối số. Điều đó sẽ cung cấp cho bạn một lỗi biên dịch do phụ thuộc vòng tròn. Ngoài ra, bạn sẽ không có những quá tải này cho Hàm và Chức năng BiFunction. Do đó, bạn cũng nên định nghĩa Func với một đối số và Func với hai đối số. Trong .NET, các phụ thuộc vòng tròn sẽ bị phá vỡ bằng cách sử dụng các phương thức mở rộng không có trong Java.


2
Tại sao bạn cần andThenvới 16 đối số? Kết quả của một hàm trong Java là một giá trị duy nhất. andThennhận giá trị này và làm điều gì đó với nó. Ngoài ra, không có vấn đề gì với việc đặt tên. Các tên lớp phải khác nhau và nằm trong các tệp khác nhau có tên giống nhau - tuân theo logic do các nhà phát triển ngôn ngữ Java thiết lập với Hàm và Chức năng BiFunction. Ngoài ra, tất cả các tên khác nhau này là cần thiết nếu các loại đối số khác nhau. Người ta có thể tạo ra một VargFunction(T, R) { R apply(T.. t) ... }loại duy nhất.
Alex Pakka

2

Tôi đã tìm thấy mã nguồn cho BiFunction ở đây:

https://github.com/JetBrains/jdk8u_jdk/blob/master/src/share/classes/java/util/ Chức năng/BiFunction.java

Tôi đã sửa đổi nó để tạo TriFunction. Giống như BiFunction, nó sử dụng andThen () chứ không phải compile (), vì vậy đối với một số ứng dụng yêu cầu compile (), nó có thể không phù hợp. Nó sẽ ổn đối với các loại đối tượng bình thường. Bạn có thể tìm thấy một bài viết hay về andThen () và compile () tại đây:

http://www.deadcodehesia.com/2015-09-07-java-8-funcation-composition-using-compose-and-andthen/

import java.util.Objects;
import java.util.function.Function;

/**
 * Represents a function that accepts two arguments and produces a result.
 * This is the three-arity specialization of {@link Function}.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object, Object)}.
 *
 * @param <S> the type of the first argument to the function
 * @param <T> the type of the second argument to the function
 * @param <U> the type of the third argument to the function
 * @param <R> the type of the result of the function
 *
 * @see Function
 * @since 1.8
 */
@FunctionalInterface
public interface TriFunction<S, T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param s the first function argument
     * @param t the second function argument
     * @param u the third function argument
     * @return the function result
     */
    R apply(S s, T t, U u);

    /**
     * Returns a composed function that first applies this function to
     * its input, and then applies the {@code after} function to the result.
     * If evaluation of either function throws an exception, it is relayed to
     * the caller of the composed function.
     *
     * @param <V> the type of output of the {@code after} function, and of the
     *           composed function
     * @param after the function to apply after this function is applied
     * @return a composed function that first applies this function and then
     * applies the {@code after} function
     * @throws NullPointerException if after is null
     */
    default <V> TriFunction<S, T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (S s, T t, U u) -> after.apply(apply(s, t, u));
    }
}

2

Bạn cũng có thể tạo hàm của riêng mình với 3 tham số

@FunctionalInterface
public interface MiddleInterface<F,T,V>{
    boolean isBetween(F from, T to, V middleValue);
}

MiddleInterface<Integer, Integer, Integer> middleInterface = 
(x,y,z) -> x>=y && y<=z; // true

0

Không phải lúc nào bạn cũng có thể dừng lại ở TriFunction. Đôi khi, bạn có thể cần chuyển n số tham số cho các hàm của mình. Sau đó, nhóm hỗ trợ sẽ phải tạo một QuadFunction để sửa mã của bạn. Giải pháp lâu dài sẽ là tạo một Đối tượng với các tham số bổ sung và sau đó sử dụng Hàm hoặc Chức năng BiFunction được tạo sẵn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.