1678893202
Tìm hiểu cách triển khai các khái niệm về Functional Programming (FP) trong Java, bao gồm xem các hàm như công dân hạng nhất, xâu chuỗi và kết hợp chúng để tạo các đường dẫn hàm.
Lập trình chức năng (FP) là một mô hình lập trình. Nó nhấn mạnh việc sử dụng các hàm thuần túy không có tác dụng phụ và luôn trả về cùng một đầu ra cho một đầu vào nhất định.
Bài viết này tìm hiểu cách triển khai các khái niệm FP trong Java, bao gồm xem các hàm như công dân hạng nhất, xâu chuỗi và kết hợp chúng để tạo các đường dẫn chức năng.
Chúng ta cũng sẽ thảo luận về kỹ thuật currying, cho phép một hàm nhận nhiều đối số được chuyển đổi thành một chuỗi các hàm mà mỗi hàm nhận một đối số. Điều này có thể đơn giản hóa việc sử dụng các chức năng phức tạp và làm cho chúng có thể tái sử dụng nhiều hơn.
Trong bài viết này, tôi sẽ chỉ cho bạn các ví dụ về cách triển khai các khái niệm này trong Java bằng cách sử dụng các tính năng ngôn ngữ hiện đại, như “java.util.function.Function”, “java.util.function.BiFunction” và TriFunction do người dùng định nghĩa . Sau đó, chúng ta sẽ xem cách khắc phục hạn chế của nó bằng cách sử dụng cà ri.
Mục lục:
Giao diện java.util.function.Function là thành phần chính của API lập trình chức năng Java 8. java.util.function.Function là một giao diện chức năng trong Java nhận đầu vào thuộc loại 'T' và tạo ra đầu ra thuộc loại 'R'.
Trong lập trình hàm, các hàm là công dân hạng nhất, nghĩa là chúng có thể được truyền xung quanh dưới dạng giá trị, được lưu trữ trong các biến hoặc cấu trúc dữ liệu và được sử dụng làm đối số hoặc giá trị trả về của các hàm khác.
Giao diện chức năng cung cấp một cách để xác định và thao tác các chức năng trong mã Java.
Giao diện chức năng đại diện cho một chức năng nhận một đầu vào và tạo ra một đầu ra. Nó có một phương thức trừu tượng duy nhất, apply()lấy một đối số của một loại đã chỉ định và trả về kết quả của một loại đã chỉ định.
Bạn có thể sử dụng giao diện chức năng để xác định các chức năng mới, cũng như thao tác và soạn các chức năng hiện có. Ví dụ: bạn có thể sử dụng nó để chuyển đổi danh sách các đối tượng thuộc một loại thành danh sách các đối tượng thuộc loại khác hoặc để áp dụng một loạt các phép biến đổi cho một luồng dữ liệu.
Một trong những lợi ích chính của giao diện chức năng là nó cho phép bạn viết mã ngắn gọn và biểu cảm hơn. Bằng cách xác định các hàm dưới dạng giá trị và chuyển chúng xung quanh dưới dạng đối số hoặc giá trị trả về, nhà phát triển có thể tạo nhiều mã mô-đun hơn và có thể tái sử dụng. Ngoài ra, bằng cách sử dụng lambdas để xác định hàm, mã Java có thể biểu cảm hơn và dễ đọc hơn.
Bạn có thể nghĩ về java.util.function.Functionnhư thế này:
Chức năng<A, B>
Java.util.function.BiFunction cũng là một giao diện chức năng trong Java có hai đầu vào 'T' và 'U' và tạo ra đầu ra loại 'R'. Tóm lại, BiFunction nhận 2 đầu vào và trả về một đầu ra:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Bạn có thể nghĩ về java.util.function.BiFunctionnhư thế này:
BiFunction<A, B, C>
Chuỗi chức năng là một kỹ thuật trong lập trình chức năng bao gồm việc kết hợp nhiều chức năng vào một đường dẫn hoặc chuỗi đơn lẻ.
Trong Java FP, chuỗi chức năng thường được sử dụng để chuyển đổi dữ liệu theo một loạt các bước, trong đó mỗi bước áp dụng một chuyển đổi cụ thể cho dữ liệu và chuyển nó sang bước tiếp theo trong chuỗi.
Giao diện chức năng trong Java cung cấp một công cụ mạnh mẽ để xâu chuỗi chức năng. Mỗi chức năng trong chuỗi được định nghĩa là một phiên bản riêng của giao diện Chức năng. Đầu ra của mỗi chức năng trở thành đầu vào cho chức năng tiếp theo trong chuỗi. Điều này cho phép một loạt phép biến đổi được áp dụng cho dữ liệu, lần lượt, cho đến khi tạo ra kết quả cuối cùng.
Phương thức "andThen()" là một phương thức mặc định được cung cấp bởi giao diện chức năng trong Java. Phương thức này lấy một chuỗi gồm hai hàm và áp dụng chúng liên tiếp, sử dụng đầu ra của hàm thứ nhất làm đầu vào cho hàm thứ hai. Chuỗi các chức năng này dẫn đến một chức năng mới kết hợp hành vi của cả hai chức năng trong một chuyển đổi duy nhất.
Cú pháp của andThentrông như thế này:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
và sau đó
Chúng ta có thể chia nhỏ ví dụ thành một loạt các bước. Ban đầu, chức năng "nhân" sẽ được thực thi và đầu ra của nó được chuyển làm đầu vào cho chức năng "thêm". Sau đó, chức năng "logOutput" được sử dụng để ghi kết quả đầu ra.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
Đoạn mã trên giới thiệu việc tạo ba hàm, cụ thể là nhân, cộng và logOutput.
Hàm multiplychấp nhận đầu vào số nguyên và tạo đầu ra số nguyên, giống như addhàm. Tuy nhiên, logOutputhàm chấp nhận đầu vào số nguyên và không trả về gì (như được biểu thị bằng đối tượng Đơn vị, hàm ý không có giá trị).
Hàm executexâu chuỗi ba hàm lại với nhau, trong đó đầu ra của phép nhân được sử dụng làm đầu vào cho hàm thêm và kết quả đầu ra của phép cộng được chuyển đến hàm logOutput cho mục đích ghi nhật ký. Ví dụ trên giới thiệu chức năng xâu chuỗi bằng andThenphương thức mặc định.
Không giống như andThenphương thức, composephương thức là một phương thức mặc định khác được cung cấp bởi giao diện hàm trong Java. Nó áp dụng chức năng đầu tiên cho đầu ra của chức năng thứ hai.
Điều này có nghĩa là chức năng thứ hai được áp dụng cho đầu vào trước, sau đó chức năng đầu tiên được áp dụng cho đầu ra của chức năng thứ hai. Kết quả là, một chuỗi chức năng được tạo ra trong đó đầu ra của chức năng thứ hai trở thành đầu vào của chức năng thứ nhất.
Chức năng soạn thảo trông như thế này:
default Function<V, R> compose(Function<? super V, ? extends T> before)
soạn, biên soạn
Khi nào nên sử dụng andThenvs composetùy thuộc vào thứ tự bạn muốn các chức năng được áp dụng.
Nếu bạn muốn áp dụng các chức năng theo thứ tự chúng được xác định, từ trái sang phải, thì bạn nên sử dụng phương andThenpháp này. Phương pháp này áp dụng hàm đầu tiên cho đầu vào, sau đó áp dụng hàm thứ hai cho đầu ra của hàm đầu tiên.
Điều này hữu ích khi bạn muốn xâu chuỗi các chức năng xử lý dữ liệu đầu vào theo một thứ tự cụ thể.
Mặt khác, nếu bạn muốn áp dụng các chức năng theo thứ tự ngược lại, từ phải sang trái, thì bạn nên sử dụng phương composepháp. Phương pháp này áp dụng hàm thứ hai cho đầu vào, sau đó áp dụng hàm đầu tiên cho đầu ra của hàm thứ hai.
Điều này hữu ích khi bạn muốn xâu chuỗi các chức năng lại với nhau cần được áp dụng theo thứ tự ngược lại với cách chúng được xác định.
Nói chung, bạn nên sử dụng andThenkhi bạn muốn áp dụng các hàm theo thứ tự chúng được xác định và sử dụng composekhi bạn muốn áp dụng các hàm theo thứ tự ngược lại. Tuy nhiên, sự lựa chọn giữa hai phương pháp cuối cùng phụ thuộc vào các yêu cầu cụ thể trong trường hợp sử dụng của bạn.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
Trong composeví dụ này, các chức năng được thực hiện theo thứ tự từ phải sang trái. Đầu tiên, multiplyđược thực thi và đầu ra của nó được chuyển đến addhàm. Sau đó, kết quả đầu ra của addhàm được chuyển đến logOutputcho mục đích ghi nhật ký.
Phương apply()thức nhận một đầu vào và trả về một kết quả. Nó được sử dụng để áp dụng một hàm cho một đối số và tính toán kết quả.
Hàm apply()là một phương thức của các giao diện chức năng, chẳng hạn như giao diện hàm, lấy một đối số của một loại được chỉ định và trả về kết quả của một loại được chỉ định. Đây là phương thức trừu tượng duy nhất của các giao diện này, được yêu cầu để chúng được sử dụng làm giao diện chức năng.
Chức apply()năng xác định hành vi của giao diện chức năng. Khi một phiên bản của giao diện chức năng được tạo, apply()chức năng được triển khai để xác định chức năng của giao diện chức năng khi nó được gọi với một đối số.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
Trong đoạn mã này, chúng tôi định nghĩa một hàm được gọi multiplylà nhận một đối số số nguyên và trả về bình phương của số nguyên đó. Sau đó, chúng ta gọi apply()hàm trên hàm này, chuyển vào giá trị nguyên là 5. apply()Hàm thực thi biểu thức lambda được xác định trong squareHàm và trả về kết quả tính toán, trong trường hợp này là 25.
Hãy hiểu bằng cách xem một ví dụ thực tế, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
Đoạn mã trên định nghĩa một lớp FileReaderPipeline, lớp này đọc các dòng từ một tệp, xử lý chúng thông qua một chuỗi các hàm và in kết quả đầu ra.
Đầu tiên, ba hàm trim, toUpperCasevà replaceSpecialCharactersđược xác định bằng tham chiếu phương thức và biểu thức lambda. trimxóa khoảng trắng ở đầu và cuối khỏi Chuỗi, toUpperCasechuyển đổi chuỗi thành chữ hoa và replaceSpecialCharactersxóa mọi ký tự không thuộc bảng chữ cái khỏi chuỗi.
Tiếp theo, một đường dẫn chức năng được tạo bằng cách sử dụng andThenphương thức này để xâu chuỗi ba chức năng lại với nhau. Đầu vào của hàm đường ống là Chuỗi và đầu ra cũng là Chuỗi. Khi một Chuỗi được chuyển đến hàm đường dẫn, trước tiên, hàm này sẽ xóa mọi khoảng trắng ở đầu hoặc ở cuối, sau đó chuyển đổi chuỗi thành chữ hoa và cuối cùng xóa mọi ký tự không thuộc bảng chữ cái.
Cuối cùng, trong mainphương thức này, chương trình sẽ đọc các dòng từ một tệp (trong ví dụ này là "example.txt") bằng cách sử dụng tệp BufferedReader. Mỗi dòng được xử lý thông qua chức năng đường ống bằng applyphương pháp áp dụng từng chức năng trong đường ống theo trình tự và trả về kết quả đầu ra. Đầu ra kết quả sau đó được in ra bàn điều khiển bằng cách sử dụng System.out.println.
Gói Java util chứa hai giao diện chức năng được gọi là "Function<A, B>" và "BiFunction<A, B, C>". Giao Functiondiện nhận một đầu vào duy nhất và tạo ra một đầu ra, trong khi BiFunctiongiao diện nhận hai đầu vào và tạo ra một đầu ra. Đây là một minh họa:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Đây là một ràng buộc rõ ràng vì chỉ có hai chức năng, với một chức năng chấp nhận một đầu vào duy nhất và chức năng kia chấp nhận hai đầu vào. Do đó, nếu chúng ta cần một hàm nhận ba đầu vào, chúng ta phải xây dựng hàm tùy chỉnh của riêng mình.
Để làm được điều này, hãy thiết kế một hàm chấp nhận ba đầu vào và đặt tên cho nó là TriFunction. Đây là một ví dụ thực hiện:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
Trong các tình huống mà chúng tôi yêu cầu nhiều tham số hơn những gì các chức năng có sẵn có thể chấp nhận, chúng tôi có thể tận dụng tính năng cà ri để khắc phục giới hạn này.
Chúng ta có thể cấu trúc lại hàm trước đó bằng cách sử dụng currying, bao gồm việc phân tách một hàm nhận nhiều đối số thành một chuỗi các hàm, mỗi hàm chỉ chấp nhận một đối số. Điều này phù hợp với định nghĩa về currying, là một kỹ thuật được sử dụng trong Lập trình hàm.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
Ở trên khai báo một Functiontên mới sumWithThreeParamscó đầu Integervào. Nó trả về một Functioncái mới nhận một Integerđầu vào và trả về một cái mới khác Functionlấy một Integerđầu vào và trả về một Integerđầu ra. Tương tự, một Hàm mới có tên sumWithFourParamsđược tạo.
Lưu ý rằng hàm curried sumWithThreeParamsvà sumWithFourParamscho phép chúng ta áp dụng một phần đối số cho hàm, điều này có thể hữu ích trong trường hợp chúng ta có sẵn một số đầu vào nhưng không phải tất cả chúng
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
Trong bài viết này, chúng tôi đã đề cập đến việc triển khai các khái niệm lập trình chức năng (FP) trong Java, chẳng hạn như coi các hàm là công dân hạng nhất, kết hợp chúng thành các đường ống và sử dụng currying để đơn giản hóa các hàm phức tạp.
Chúng tôi đã cung cấp các ví dụ sử dụng các tính năng ngôn ngữ hiện đại như java.util.function.Functionvà java.util.function.BiFunction, cũng như TriFunctiontính năng currying do người dùng xác định để khắc phục các hạn chế của nó.
Bằng cách áp dụng các kỹ thuật này, các nhà phát triển có thể viết mã ngắn gọn hơn, có thể tái sử dụng và có thể bảo trì bằng Java.
Với sự phổ biến ngày càng tăng của FP trong ngành, việc hiểu và triển khai các khái niệm này ngày càng trở nên quan trọng đối với các nhà phát triển Java.
Sau khi đọc bài viết này, nếu bạn thấy nó hữu ích và muốn khám phá thêm các ví dụ thực tế, vui lòng truy cập kho lưu trữ và cân nhắc cho nó một ngôi sao.
Nguồn: https://www.freecodecamp.org
#java #functionalprogramming
1678885869
Aprenda a implementar conceitos de programação funcional (FP) em Java, incluindo a exibição de funções como cidadãos de primeira classe, encadeamento e composição para criar pipelines de funções.
A programação funcional (PF) é um paradigma de programação. Ele enfatiza o uso de funções puras que não têm efeitos colaterais e sempre retornam a mesma saída para uma determinada entrada.
Este artigo explora como implementar conceitos de FP em Java, incluindo a exibição de funções como cidadãos de primeira classe, encadeamento e composição para criar pipelines de funções.
Também discutiremos a técnica de currying, que permite que uma função que recebe vários argumentos seja transformada em uma cadeia de funções, cada uma com um único argumento. Isso pode simplificar o uso de funções complexas e torná-las mais reutilizáveis.
Neste artigo, mostrarei exemplos de como implementar esses conceitos em Java usando recursos de linguagem moderna, como “java.util.function.Function”, “java.util.function.BiFunction” e um TriFunction definido pelo usuário . Veremos então como superar sua limitação usando currying.
Índice:
A interface java.util.function.Function é um componente chave da API de programação funcional Java 8. O java.util.function.Function é uma interface funcional em Java que recebe uma entrada do tipo 'T' e produz uma saída do tipo 'R'.
Na programação funcional, as funções são cidadãs de primeira classe, o que significa que podem ser passadas como valores, armazenadas em variáveis ou estruturas de dados e usadas como argumentos ou valores de retorno de outras funções.
A interface de função fornece uma maneira de definir e manipular funções no código Java.
A interface de função representa uma função que recebe uma entrada e produz uma saída. Ele tem um único método abstrato, apply(), que recebe um argumento de um tipo especificado e retorna um resultado de um tipo especificado.
You can use the function interface to define new functions, as well as to manipulate and compose existing functions. For example, you can use it to convert a list of objects of one type to a list of objects of another type, or to apply a series of transformations to a stream of data.
One of the main benefits of the function interface is that it allows you to write code that is more concise and expressive. By defining functions as values and passing them around as arguments or return values, developers can create more modular and reusable code. Also, by using lambdas to define functions, Java code can be more expressive and easier to read.
You can think about java.util.function.Function like this:
Function<A, B>
The java.util.function.BiFunction is also a functional interface in Java that takes two inputs 'T' and 'U' and produces an output of type 'R'. In short, BiFunction takes 2 inputs and returns an output:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
You can think about java.util.function.BiFunction like this:
BiFunction<A, B, C>
Function chaining is a technique in functional programming that involves composing multiple functions into a single pipeline or chain.
In Java FP, function chaining is often used to transform data in a series of steps, where each step applies a specific transformation to the data and passes it on to the next step in the chain.
The function interface in Java provides a powerful tool for function chaining. Each function in the chain is defined as a separate instance of the Function interface. The output of each function becomes the input to the next function in the chain. This allows for a series of transformations to be applied to the data, one after another, until the final result is produced.
The "andThen()" method is a default method provided by the function interface in Java. This method takes a sequence of two functions and applies them in succession, using the output of the first function as the input to the second function. This chaining of the functions results in a new function that combines the behavior of both functions in a single transformation.
The syntax of andThen looks like this:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
andThen
We can break the example down into series of steps. Initially the "multiply" function will be executed and its output passed as input to the "add" function. Then the "logOutput" function is used to log the resulting output.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
The the above code snippet showcases the creation of three functions, namely multiply, add, and logOutput.
The function multiply accepts an integer input and produces an integer output, just like the add function. But, the logOutput function accepts an integer input and returns nothing (as represented by the Unit object, which implies the absence of a value).
The execute function chains the three functions together, where the output of multiply is utilized as the input for the add function, and the resulting output of add is passed to the logOutput function for logging purposes. The above example showcases function chaining using the andThen default method.
Unlike the andThen method, the compose method is another default method provided by the function interface in Java. It applies the first function to the output of the second function.
This means that the second function is applied to the input first, and then the first function is applied to the output of the second function. As a result, a chain of functions is created where the output of the second function becomes the input of the first function.
The compose function looks like this:
default Function<V, R> compose(Function<? super V, ? extends T> before)
compose
When to use andThen vs compose depends on the order in which you want the functions to be applied.
If you want to apply the functions in the order they are defined, from left to right, then you should use the andThen method. This method applies the first function to the input, and then applies the second function to the output of the first function.
This is useful when you want to chain together functions that process the input data in a specific order.
On the other hand, if you want to apply the functions in the opposite order, from right to left, then you should use the compose method. This method applies the second function to the input, and then applies the first function to the output of the second function.
This is useful when you want to chain together functions that need to be applied in the opposite order of the way they are defined.
In general, you should use andThen when you want to apply the functions in the order they are defined, and use compose when you want to apply the functions in the opposite order. But the choice between the two methods ultimately depends on the specific requirements of your use case.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
In the compose example, the functions are executed in a right-to-left order. Firstly, multiply is executed and its output is passed to the add function. Then, the resulting output of the add function is passed to logOutput for logging purposes.
The apply() method takes an input and returns a result. It is used to apply a function to an argument and compute a result.
The apply() function is a method of functional interfaces, such as the function interface, that takes an argument of a specified type and returns a result of a specified type. It is the single abstract method of these interfaces, which is required for them to be used as functional interfaces.
The apply() function defines the behavior of the functional interface. When an instance of a functional interface is created, the apply() function is implemented to define what the functional interface does when it is called with an argument.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
Neste código, definimos uma função chamada multiplyque recebe um argumento inteiro e retorna o quadrado desse inteiro. Em seguida, chamamos a apply()função nesta função, passando o valor inteiro de 5. A apply()função executa a expressão lambda definida na squareFunção e retorna o resultado do cálculo, que neste caso é 25.
Vamos entender observando um exemplo do mundo real, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
O código acima define uma classe FileReaderPipeline, que lê as linhas de um arquivo, as processa por meio de um pipeline de funções e imprime a saída resultante.
Primeiro, três funções trim, toUpperCasee replaceSpecialCharacterssão definidas usando referências de método e expressões lambda. trimremove os espaços em branco iniciais e finais de uma String, toUpperCaseconverte a string em letras maiúsculas e replaceSpecialCharactersremove todos os caracteres não alfabéticos da string.
Em seguida, um pipeline de função é criado usando o andThenmétodo, que encadeia as três funções. A entrada para a função de pipeline é uma String e a saída também é uma String. Quando uma String é passada para a função de pipeline, ela primeiro remove qualquer espaço em branco à esquerda ou à direita, depois converte a string em letras maiúsculas e, finalmente, remove todos os caracteres não alfabéticos.
Por fim, no mainmétodo, o programa lê linhas de um arquivo (neste exemplo, "exemplo.txt") usando um arquivo BufferedReader. Cada linha é processada por meio da função de pipeline usando o applymétodo, que aplica cada função no pipeline em sequência e retorna a saída resultante. A saída resultante é impressa no console usando System.out.println.
O pacote Java util contém duas interfaces funcionais conhecidas como "Function<A, B>" e "BiFunction<A, B, C>". A Functioninterface recebe uma única entrada e produz uma saída, enquanto a BiFunctioninterface recebe duas entradas e produz uma saída. Aqui está uma ilustração:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Essa é uma restrição clara, pois há apenas duas funções, uma aceitando uma única entrada e a outra aceitando duas entradas. Conseqüentemente, se precisarmos de uma função que receba três entradas, devemos construir nossa própria função personalizada.
Para isso, vamos projetar uma função que aceita três entradas e nomeá-la TriFunction. Aqui está um exemplo de implementação:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
Em situações em que exigimos mais parâmetros do que as funções disponíveis podem aceitar, podemos aproveitar o currying para superar essa limitação.
Podemos refatorar a função anterior usando currying, que envolve a decomposição de uma função que recebe vários argumentos em uma sequência de funções, cada uma das quais aceita apenas um argumento. Isso está de acordo com a definição de currying, que é uma técnica empregada na Programação Funcional.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
O acima declara um novo Functionnome sumWithThreeParamsque recebe uma Integerentrada. Ele retorna um new Functionque recebe uma Integerentrada e retorna outro new Functionque recebe uma Integerentrada e retorna uma Integersaída. Da mesma forma, uma nova Função nomeada sumWithFourParamsé criada.
Observe que a função curried sumWithThreeParamse sumWithFourParamsnos permite aplicar parcialmente argumentos à função, o que pode ser útil em situações em que temos algumas das entradas disponíveis, mas não todas
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
Neste artigo, abordamos a implementação de conceitos de programação funcional (FP) em Java, como tratar funções como cidadãos de primeira classe, compô-los em pipelines e utilizar currying para simplificar funções complexas.
Fornecemos exemplos usando recursos de linguagem moderna como java.util.function.Functione java.util.function.BiFunction, bem como um TriFunctioncurrying definido pelo usuário para superar suas limitações.
Aplicando essas técnicas, os desenvolvedores podem escrever um código mais conciso, reutilizável e sustentável em Java.
Com a crescente popularidade do PF na indústria, entender e implementar esses conceitos está se tornando cada vez mais importante para os desenvolvedores Java.
Depois de ler este artigo, se você achou útil e gostaria de explorar mais exemplos práticos, visite o repositório e considere dar uma estrela.
Fonte: https://www.freecodecamp.org
#java #functionalprogramming
1678882260
함수를 일급 시민으로 보고 함수 파이프라인을 만들기 위해 함수를 연결하고 구성하는 것을 포함하여 Java에서 함수형 프로그래밍(FP) 개념을 구현하는 방법을 알아봅니다.
함수형 프로그래밍(FP)은 프로그래밍 패러다임입니다. 부작용이 없고 주어진 입력에 대해 항상 동일한 출력을 반환하는 순수 함수의 사용을 강조합니다.
이 기사에서는 함수를 일급 시민으로 보고 함수 파이프라인을 만들기 위해 함수를 연결하고 구성하는 것을 포함하여 Java에서 FP 개념을 구현하는 방법을 살펴봅니다.
또한 여러 인수를 받는 함수를 각각 단일 인수를 받는 일련의 함수로 변환할 수 있는 커링(currying) 기술에 대해서도 설명합니다. 이를 통해 복잡한 함수의 사용을 단순화하고 재사용 가능성을 높일 수 있습니다.
이 기사에서는 "java.util.function.Function", "java.util.function.BiFunction" 및 사용자 정의 TriFunction과 같은 최신 언어 기능을 사용하여 Java에서 이러한 개념을 구현하는 방법의 예를 보여줍니다. . 그런 다음 커링을 사용하여 한계를 극복하는 방법을 살펴보겠습니다.
목차:
java.util.function.Function 인터페이스는 Java 8 기능 프로그래밍 API의 핵심 구성 요소입니다. java.util.function.Function은 'T' 유형의 입력을 받아 'R' 유형의 출력을 생성하는 Java의 기능적 인터페이스입니다.
함수형 프로그래밍에서 함수는 일급 시민입니다. 즉, 값으로 전달되고 변수 또는 데이터 구조에 저장되며 다른 함수의 인수 또는 반환 값으로 사용될 수 있습니다.
함수 인터페이스는 Java 코드에서 함수를 정의하고 조작하는 방법을 제공합니다.
함수 인터페이스는 하나의 입력을 받아 하나의 출력을 생성하는 함수를 나타냅니다. apply()여기 에는 지정된 유형의 인수를 취하고 지정된 유형의 결과를 반환하는 단일 추상 메서드가 있습니다 .
함수 인터페이스를 사용하여 새 함수를 정의하고 기존 함수를 조작 및 구성할 수 있습니다. 예를 들어 한 유형의 개체 목록을 다른 유형의 개체 목록으로 변환하거나 일련의 변환을 데이터 스트림에 적용하는 데 사용할 수 있습니다.
함수 인터페이스의 주요 이점 중 하나는 더 간결하고 표현력이 풍부한 코드를 작성할 수 있다는 것입니다. 함수를 값으로 정의하고 이를 인수 또는 반환 값으로 전달함으로써 개발자는 보다 모듈화되고 재사용 가능한 코드를 만들 수 있습니다. 또한 람다를 사용하여 함수를 정의함으로써 Java 코드를 보다 표현력 있고 읽기 쉽게 만들 수 있습니다.
다음과 같이 생각할 수 있습니다 java.util.function.Function.
기능<A,B>
java.util.function.BiFunction은 또한 두 개의 입력 'T'와 'U'를 받아 'R' 유형의 출력을 생성하는 Java의 기능적 인터페이스입니다. 즉, BiFunction은 2개의 입력을 받아 출력을 반환합니다.
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
다음과 같이 생각할 수 있습니다 java.util.function.BiFunction.
BiFunction<A, B, C>
함수 연결은 여러 함수를 단일 파이프라인 또는 체인으로 구성하는 함수형 프로그래밍 기술입니다.
Java FP에서 함수 연결은 종종 일련의 단계에서 데이터를 변환하는 데 사용되며 각 단계는 특정 변환을 데이터에 적용하고 이를 체인의 다음 단계로 전달합니다.
Java의 함수 인터페이스는 함수 연결을 위한 강력한 도구를 제공합니다. 체인의 각 함수는 Function 인터페이스의 별도 인스턴스로 정의됩니다. 각 함수의 출력은 체인의 다음 함수에 대한 입력이 됩니다. 이를 통해 최종 결과가 생성될 때까지 일련의 변환을 데이터에 차례로 적용할 수 있습니다.
"andThen()" 메서드는 Java의 함수 인터페이스에서 제공하는 기본 메서드입니다. 이 메서드는 두 함수의 시퀀스를 가져와서 첫 번째 함수의 출력을 두 번째 함수의 입력으로 사용하여 연속적으로 적용합니다. 이러한 함수 연결은 단일 변환에서 두 함수의 동작을 결합하는 새로운 함수를 생성합니다.
의 구문은 andThen다음과 같습니다.
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
그런 다음
예제를 일련의 단계로 나눌 수 있습니다. 처음에는 "곱하기" 기능이 실행되고 그 출력이 "추가" 기능에 대한 입력으로 전달됩니다. 그런 다음 "logOutput" 기능을 사용하여 결과 출력을 기록합니다.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
위의 코드 스니펫은 곱셈, 추가 및 logOutput이라는 세 가지 함수 생성을 보여줍니다.
이 함수는 함수 multiply와 마찬가지로 정수 입력을 받고 정수 출력을 생성합니다 add. 그러나 이 logOutput함수는 정수 입력을 받아들이고 아무것도 반환하지 않습니다(값이 없음을 의미하는 Unit 개체로 표시됨).
이 execute함수는 세 함수를 함께 연결합니다. 여기서 곱셈의 출력은 더하기 함수의 입력으로 사용되며 추가의 결과 출력은 로깅을 위해 logOutput 함수로 전달됩니다. 위의 예는 andThen기본 방법을 사용한 함수 연결을 보여줍니다.
andThen메소드 와 달리 compose메소드는 Java의 함수 인터페이스에서 제공하는 또 다른 기본 메소드입니다. 첫 번째 함수를 두 번째 함수의 출력에 적용합니다.
이것은 두 번째 함수가 먼저 입력에 적용되고 첫 번째 함수가 두 번째 함수의 출력에 적용됨을 의미합니다. 결과적으로 두 번째 함수의 출력이 첫 번째 함수의 입력이 되는 일련의 함수가 생성됩니다.
작성 기능은 다음과 같습니다.
default Function<V, R> compose(Function<? super V, ? extends T> before)
구성하다
andThenvs를 사용할 시기는 compose함수를 적용하려는 순서에 따라 다릅니다.
함수를 정의된 순서대로 왼쪽에서 오른쪽으로 적용하려면 이 andThen방법을 사용해야 합니다. 이 방법은 첫 번째 함수를 입력에 적용한 다음 두 번째 함수를 첫 번째 함수의 출력에 적용합니다.
이는 특정 순서로 입력 데이터를 처리하는 함수를 함께 연결하려는 경우에 유용합니다.
반대로 오른쪽에서 왼쪽으로 반대 순서로 기능을 적용하려면 이 compose방법을 사용해야 합니다. 이 방법은 입력에 두 번째 함수를 적용한 다음 두 번째 함수의 출력에 첫 번째 함수를 적용합니다.
이는 정의된 방식과 반대 순서로 적용해야 하는 함수를 함께 연결하려는 경우에 유용합니다.
andThen일반적으로 정의된 순서대로 기능을 적용하고 싶을 때 사용 하고 compose역순으로 기능을 적용하고 싶을 때 사용합니다. 그러나 두 방법 중 선택은 궁극적으로 사용 사례의 특정 요구 사항에 따라 달라집니다.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
예제 에서 compose함수는 오른쪽에서 왼쪽 순서로 실행됩니다. 먼저 multiply가 실행되고 출력이 함수로 전달됩니다 add. 그런 다음 함수의 결과 출력이 로깅 목적으로 add전달됩니다 .logOutput
메서드 apply()는 입력을 받아 결과를 반환합니다. 인수에 함수를 적용하고 결과를 계산하는 데 사용됩니다.
함수는 apply()지정된 유형의 인수를 취하고 지정된 유형의 결과를 반환하는 함수 인터페이스와 같은 기능 인터페이스의 메서드입니다. 기능 인터페이스로 사용하기 위해 필요한 이러한 인터페이스의 단일 추상 메서드입니다.
함수 apply()는 기능 인터페이스의 동작을 정의합니다. 기능적 인터페이스의 인스턴스가 생성될 때 apply()인수와 함께 호출될 때 기능적 인터페이스가 수행하는 작업을 정의하기 위해 함수가 구현됩니다.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
multiply이 코드에서는 정수 인수를 사용하고 해당 정수의 제곱을 반환하는 함수를 정의합니다 . 그런 다음 이 함수에서 함수를 호출하여 apply()정수 값 5를 전달합니다. 함수는 함수 apply()에 정의된 람다 식을 실행 square하고 계산 결과를 반환합니다. 이 경우에는 25입니다.
실제 예제인 FileReaderPipeline을 살펴보고 이해해 봅시다.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
위의 코드는 FileReaderPipeline 클래스를 정의합니다. 이 클래스는 파일에서 줄을 읽고 함수 파이프라인을 통해 처리하고 결과 출력을 인쇄합니다.
먼저 메서드 참조와 람다 식을 사용하여 세 개의 함수 trim, toUpperCase및 를 replaceSpecialCharacters정의합니다. trim문자열에서 선행 및 후행 공백을 제거하고 toUpperCase문자열을 대문자로 변환하고 replaceSpecialCharacters문자열에서 알파벳이 아닌 문자를 제거합니다.
andThen다음으로 세 함수를 함께 연결하는 메서드를 사용하여 함수 파이프라인을 만듭니다 . 파이프라인 함수에 대한 입력은 문자열이고 출력도 문자열입니다. 문자열이 파이프라인 함수에 전달되면 먼저 선행 또는 후행 공백을 제거한 다음 문자열을 대문자로 변환하고 마지막으로 알파벳이 아닌 문자를 제거합니다.
마지막으로 main메서드에서 프로그램은 BufferedReader. apply각 라인은 파이프 라인의 각 함수를 순서대로 적용하고 결과 출력을 반환하는 메서드를 사용하여 파이프라인 함수를 통해 처리됩니다 . 결과 출력은 다음을 사용하여 콘솔에 인쇄됩니다 System.out.println.
Java util 패키지에는 "Function<A, B>" 및 "BiFunction<A, B, C>"라는 두 가지 기능 인터페이스가 포함되어 있습니다. 인터페이스 Function는 단일 입력을 받아 출력을 생성하는 반면 BiFunction인터페이스는 두 개의 입력을 받아 출력을 생성합니다. 다음은 그림입니다.
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
하나는 단일 입력을 받아들이고 다른 하나는 두 개의 입력을 받아들이는 두 개의 함수만 있기 때문에 이것은 분명한 제약입니다. 결과적으로 3개의 입력을 받는 함수가 필요한 경우 사용자 지정 함수를 구성해야 합니다.
이를 위해 세 개의 입력을 받는 함수를 설계하고 이름을 지정해 보겠습니다 TriFunction. 다음은 구현 예입니다.
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
사용 가능한 함수가 허용할 수 있는 것보다 더 많은 매개변수가 필요한 상황에서 커링을 활용하여 이 제한을 극복할 수 있습니다.
커링을 사용하여 이전 함수를 리팩토링할 수 있습니다. 커링은 여러 인수를 받는 함수를 각각 하나의 인수만 허용하는 일련의 함수로 분해하는 작업을 포함합니다. 이는 함수형 프로그래밍에서 사용되는 기술인 커링의 정의와 일치합니다.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
위의 내용은 입력을 받는 새 Function이름을 선언합니다 . 입력을 받는 new를 반환 하고 입력을 받고 출력 을 반환하는 또 다른 new를 반환합니다 . 마찬가지로 이름이 지정된 새 함수가 생성됩니다.sumWithThreeParamsIntegerFunctionIntegerFunctionIntegerIntegersumWithFourParams
curried 함수는 인수를 부분적으로 함수에 적용할 수 있도록 하므로 일부 입력을 사용할 수 있지만 전부는 아닌 상황에서 유용할 수 있습니다 sumWithThreeParams.sumWithFourParams
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
이 기사에서는 함수를 일급 시민으로 취급하고, 파이프라인으로 구성하고, 커링을 활용하여 복잡한 함수를 단순화하는 것과 같은 Java에서 함수형 프로그래밍(FP) 개념의 구현을 다뤘습니다.
java.util.function.Function우리는 및 와 같은 최신 언어 기능을 사용하는 예제 java.util.function.BiFunction와 그 한계를 극복하기 위해 커링으로 사용자 정의를 제공했습니다 TriFunction.
이러한 기술을 적용함으로써 개발자는 Java에서 보다 간결하고 재사용 가능하며 유지 관리 가능한 코드를 작성할 수 있습니다.
업계에서 FP의 인기가 높아짐에 따라 이러한 개념을 이해하고 구현하는 것이 Java 개발자에게 점점 더 중요해지고 있습니다.
이 기사를 읽은 후 유용하다고 생각하고 보다 실용적인 예제를 탐색하고 싶다면 저장소를 방문하여 별표를 주는 것을 고려하십시오.
#java #functionalprogramming
1678878600
Узнайте, как реализовать концепции функционального программирования (FP) в Java, в том числе рассматривать функции как первоклассные граждане, связывать их в цепочки и составлять из них конвейеры функций.
Функциональное программирование (FP) — это парадигма программирования. Он подчеркивает использование чистых функций, которые не имеют побочных эффектов и всегда возвращают один и тот же результат для заданного ввода.
В этой статье рассматривается, как реализовать концепции FP в Java, в том числе рассматривать функции как первоклассные граждане, связывать их в цепочки и составлять из них конвейеры функций.
Мы также обсудим технику каррирования, позволяющую преобразовать функцию, принимающую несколько аргументов, в цепочку функций, каждая из которых принимает один аргумент. Это может упростить использование сложных функций и сделать их более пригодными для повторного использования.
В этой статье я покажу вам примеры того, как реализовать эти концепции в Java, используя современные языковые функции, такие как «java.util.function.Function», «java.util.function.BiFunction» и определяемую пользователем функцию TriFunction. . Затем мы увидим, как преодолеть это ограничение с помощью каррирования.
Оглавление:
Интерфейс java.util.function.Function является ключевым компонентом API функционального программирования Java 8. java.util.function.Function — это функциональный интерфейс в Java, который принимает ввод типа «T» и производит вывод типа «R».
В функциональном программировании функции являются гражданами первого класса, что означает, что они могут передаваться как значения, храниться в переменных или структурах данных и использоваться в качестве аргументов или возвращаемых значений других функций.
Интерфейс функций предоставляет способ определения функций и управления ими в коде Java.
Функциональный интерфейс представляет собой функцию, которая принимает один вход и производит один выход. Он имеет единственный абстрактный метод, apply()который принимает аргумент указанного типа и возвращает результат указанного типа.
You can use the function interface to define new functions, as well as to manipulate and compose existing functions. For example, you can use it to convert a list of objects of one type to a list of objects of another type, or to apply a series of transformations to a stream of data.
One of the main benefits of the function interface is that it allows you to write code that is more concise and expressive. By defining functions as values and passing them around as arguments or return values, developers can create more modular and reusable code. Also, by using lambdas to define functions, Java code can be more expressive and easier to read.
You can think about java.util.function.Function like this:
Function<A, B>
The java.util.function.BiFunction is also a functional interface in Java that takes two inputs 'T' and 'U' and produces an output of type 'R'. In short, BiFunction takes 2 inputs and returns an output:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
You can think about java.util.function.BiFunction like this:
BiFunction<A, B, C>
Function chaining is a technique in functional programming that involves composing multiple functions into a single pipeline or chain.
In Java FP, function chaining is often used to transform data in a series of steps, where each step applies a specific transformation to the data and passes it on to the next step in the chain.
The function interface in Java provides a powerful tool for function chaining. Each function in the chain is defined as a separate instance of the Function interface. The output of each function becomes the input to the next function in the chain. This allows for a series of transformations to be applied to the data, one after another, until the final result is produced.
The "andThen()" method is a default method provided by the function interface in Java. This method takes a sequence of two functions and applies them in succession, using the output of the first function as the input to the second function. This chaining of the functions results in a new function that combines the behavior of both functions in a single transformation.
The syntax of andThen looks like this:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
andThen
We can break the example down into series of steps. Initially the "multiply" function will be executed and its output passed as input to the "add" function. Then the "logOutput" function is used to log the resulting output.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
The the above code snippet showcases the creation of three functions, namely multiply, add, and logOutput.
The function multiply accepts an integer input and produces an integer output, just like the add function. But, the logOutput function accepts an integer input and returns nothing (as represented by the Unit object, which implies the absence of a value).
The execute function chains the three functions together, where the output of multiply is utilized as the input for the add function, and the resulting output of add is passed to the logOutput function for logging purposes. The above example showcases function chaining using the andThen default method.
Unlike the andThen method, the compose method is another default method provided by the function interface in Java. It applies the first function to the output of the second function.
This means that the second function is applied to the input first, and then the first function is applied to the output of the second function. As a result, a chain of functions is created where the output of the second function becomes the input of the first function.
The compose function looks like this:
default Function<V, R> compose(Function<? super V, ? extends T> before)
compose
When to use andThen vs compose depends on the order in which you want the functions to be applied.
If you want to apply the functions in the order they are defined, from left to right, then you should use the andThen method. This method applies the first function to the input, and then applies the second function to the output of the first function.
This is useful when you want to chain together functions that process the input data in a specific order.
On the other hand, if you want to apply the functions in the opposite order, from right to left, then you should use the compose method. This method applies the second function to the input, and then applies the first function to the output of the second function.
This is useful when you want to chain together functions that need to be applied in the opposite order of the way they are defined.
In general, you should use andThen when you want to apply the functions in the order they are defined, and use compose when you want to apply the functions in the opposite order. But the choice between the two methods ultimately depends on the specific requirements of your use case.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
In the compose example, the functions are executed in a right-to-left order. Firstly, multiply is executed and its output is passed to the add function. Then, the resulting output of the add function is passed to logOutput for logging purposes.
The apply() method takes an input and returns a result. It is used to apply a function to an argument and compute a result.
The apply() function is a method of functional interfaces, such as the function interface, that takes an argument of a specified type and returns a result of a specified type. It is the single abstract method of these interfaces, which is required for them to be used as functional interfaces.
The apply() function defines the behavior of the functional interface. When an instance of a functional interface is created, the apply() function is implemented to define what the functional interface does when it is called with an argument.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
В этом коде мы определяем вызываемую функцию multiply, которая принимает целочисленный аргумент и возвращает квадрат этого целого числа. Затем мы вызываем apply()функцию для этой функции, передавая целочисленное значение 5. Функция apply()выполняет лямбда-выражение, определенное в squareфункции, и возвращает результат вычисления, который в данном случае равен 25.
Давайте разберемся на реальном примере, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
Приведенный выше код определяет класс FileReaderPipeline, который считывает строки из файла, обрабатывает их с помощью конвейера функций и печатает результирующий вывод.
Во-первых, три функции trim, toUpperCaseи replaceSpecialCharactersопределяются с помощью ссылок на методы и лямбда-выражений. trimудаляет начальные и конечные пробелы из строки, toUpperCaseпреобразует строку в верхний регистр и replaceSpecialCharactersудаляет из строки все неалфавитные символы.
Затем с помощью метода создается конвейер функций andThen, который объединяет три функции в цепочку. Входные данные конвейерной функции — это строка, и выходные данные также являются строкой. Когда строка передается конвейерной функции, она сначала удаляет все начальные и конечные пробелы, затем преобразует строку в верхний регистр и, наконец, удаляет все неалфавитные символы.
Наконец, в этом mainметоде программа считывает строки из файла (в данном примере «example.txt»), используя расширение BufferedReader. Каждая строка обрабатывается функцией конвейера с использованием applyметода, который последовательно применяет каждую функцию в конвейере и возвращает результирующий результат. Полученный результат затем выводится на консоль с помощью System.out.println.
Пакет Java util содержит два функциональных интерфейса, известных как «Function<A, B>» и «BiFunction<A, B, C>». Интерфейс Functionпринимает один вход и производит вывод, тогда как BiFunctionинтерфейс принимает два входа и производит вывод. Вот иллюстрация:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Это явное ограничение, поскольку есть только две функции, одна из которых принимает один вход, а другая — два входа. Следовательно, если нам нужна функция, принимающая три входа, мы должны создать собственную пользовательскую функцию.
С этой целью давайте разработаем функцию, которая принимает три входа, и назовем ее TriFunction. Вот пример реализации:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
В ситуациях, когда нам требуется больше параметров, чем могут принять доступные функции, мы можем использовать каррирование для преодоления этого ограничения.
Мы можем реорганизовать предыдущую функцию, используя каррирование, которое включает в себя декомпозицию функции, которая принимает несколько аргументов, в последовательность функций, каждая из которых принимает только один аргумент. Это соответствует определению каррирования, метода, используемого в функциональном программировании.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
Вышеприведенное объявляет новое Functionимя sumWithThreeParams, которое принимает Integerвходные данные. Он возвращает новое значение Function, которое принимает Integerвходные данные и возвращает еще одно новое значение Function, которое принимает Integerвходные данные и возвращает Integerвыходные данные. sumWithFourParamsТочно так же создается новая функция с именем .
Обратите внимание, что каррированная функция sumWithThreeParamsи sumWithFourParamsпозволяет нам частично применять аргументы к функции, что может быть полезно в ситуациях, когда у нас есть некоторые доступные входные данные, но не все из них.
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
В этой статье мы рассмотрели реализацию концепций функционального программирования (FP) в Java, таких как обращение с функциями как с гражданами первого класса, объединение их в конвейеры и использование каррирования для упрощения сложных функций.
Мы предоставили примеры, использующие современные языковые функции, такие как java.util.function.Functionи java.util.function.BiFunction, а также определяемые пользователем TriFunctionкаррирование для преодоления его ограничений.
Применяя эти методы, разработчики могут писать на Java более лаконичный, пригодный для повторного использования и удобный в сопровождении код.
С ростом популярности FP в отрасли понимание и реализация этих концепций становится все более важным для Java-разработчиков.
После прочтения этой статьи, если вы нашли ее полезной и хотели бы изучить больше практических примеров, посетите репозиторий и подумайте о том, чтобы поставить ей звезду.
Источник: https://www.freecodecamp.org
#java #functionalprogramming
1678874958
Java で関数型プログラミング (FP) の概念を実装する方法を学びます。これには、関数をファースト クラス シチズンとして表示する、チェーン化する、関数パイプラインを作成するために関数を構成するなどがあります。
関数型プログラミング (FP) はプログラミングのパラダイムです。副作用がなく、特定の入力に対して常に同じ出力を返す純粋な関数の使用を強調しています。
この記事では、Java で FP の概念を実装する方法について説明します。これには、関数をファースト クラス シチズンとして表示する方法、チェーン化する方法、それらを構成して関数パイプラインを作成する方法が含まれます。
また、カリー化の手法についても説明します。これにより、複数の引数を取る関数を、それぞれが 1 つの引数を取る一連の関数に変換できます。これにより、複雑な関数の使用が簡素化され、再利用しやすくなります。
この記事では、「java.util.function.Function」、「java.util.function.BiFunction」、およびユーザー定義の TriFunction などの最新の言語機能を使用して、Java でこれらの概念を実装する方法の例を示します。 . 次に、カリー化を使用してその制限を克服する方法を見ていきます。
目次:
java.util.function.Function インターフェイスは、Java 8 関数型プログラミング API の重要なコンポーネントです。java.util.function.Function は、タイプ 'T' の入力を受け取り、タイプ 'R' の出力を生成する Java の関数インターフェースです。
関数型プログラミングでは、関数は第一級市民です。つまり、値として渡したり、変数やデータ構造に格納したり、他の関数の引数や戻り値として使用したりできます。
関数インターフェイスは、Java コードで関数を定義および操作する方法を提供します。
関数インターフェイスは、1 つの入力を受け取り、1 つの出力を生成する関数を表します。apply()これには、指定された型の引数を取り、指定された型の結果を返す単一の抽象メソッド があります。
関数インターフェイスを使用して、新しい関数を定義したり、既存の関数を操作および構成したりできます。たとえば、これを使用して、あるタイプのオブジェクトのリストを別のタイプのオブジェクトのリストに変換したり、一連の変換をデータ ストリームに適用したりできます。
関数インターフェイスの主な利点の 1 つは、より簡潔で表現力豊かなコードを記述できることです。関数を値として定義し、それらを引数または戻り値として渡すことにより、開発者はよりモジュール化された再利用可能なコードを作成できます。また、ラムダを使用して関数を定義することにより、Java コードの表現力が向上し、読みやすくなります。
次のように考えることができますjava.util.function.Function。
関数<A, B>
java.util.function.BiFunction は、2 つの入力「T」と「U」を受け取り、タイプ「R」の出力を生成する、Java の関数インターフェースでもあります。つまり、BiFunction は 2 つの入力を取り、出力を返します。
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
次のように考えることができますjava.util.function.BiFunction。
BiFunction<A, B, C>
関数チェーンは、複数の関数を 1 つのパイプラインまたはチェーンに構成する関数型プログラミングの手法です。
Java FP では、一連のステップでデータを変換するために関数チェーンがよく使用されます。各ステップは特定の変換をデータに適用し、チェーンの次のステップに渡します。
Java の関数インターフェイスは、関数チェーンのための強力なツールを提供します。チェーン内の各関数は、Function インターフェイスの個別のインスタンスとして定義されます。各関数の出力は、チェーン内の次の関数への入力になります。これにより、最終結果が生成されるまで、一連の変換を次々とデータに適用できます。
「andThen()」メソッドは、Java の関数インターフェースによって提供されるデフォルトのメソッドです。このメソッドは、一連の 2 つの関数を受け取り、最初の関数の出力を 2 番目の関数への入力として使用して、それらを連続して適用します。この関数の連鎖により、両方の関数の動作を 1 つの変換で結合する新しい関数が作成されます。
の構文はandThen次のようになります。
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
その後
この例を一連のステップに分解できます。最初に「乗算」関数が実行され、その出力が「加算」関数への入力として渡されます。次に、「logOutput」関数を使用して、結果の出力をログに記録します。
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
上記のコード スニペットは、multiply、add、および logOutput という 3 つの関数の作成を示しています。
関数はmultiply整数入力を受け入れ、add関数と同様に整数出力を生成します。ただし、logOutput関数は整数入力を受け入れ、何も返しません (Unit オブジェクトで表されるように、値がないことを意味します)。
このexecute関数は 3 つの関数を連鎖させます。そこでは、multiply の出力が add 関数の入力として利用され、add の結果の出力がログ記録のために logOutput 関数に渡されます。上記の例は、andThenデフォルトの方法を使用した関数チェーンを示しています。
メソッドとは異なりandThen、composeメソッドは Java の関数インターフェイスによって提供される別のデフォルト メソッドです。最初の関数を 2 番目の関数の出力に適用します。
これは、最初に 2 番目の関数が入力に適用され、次に最初の関数が 2 番目の関数の出力に適用されることを意味します。その結果、関数のチェーンが作成され、2 番目の関数の出力が最初の関数の入力になります。
構成関数は次のようになります。
default Function<V, R> compose(Function<? super V, ? extends T> before)
作成する
andThenvsをいつ使用するかは、compose関数を適用する順序によって異なります。
関数を定義されている順序で左から右に適用する場合は、メソッドを使用する必要がありますandThen。このメソッドは、最初の関数を入力に適用してから、2 番目の関数を最初の関数の出力に適用します。
これは、入力データを特定の順序で処理する関数を連鎖させたい場合に便利です。
一方、逆の順序で関数を右から左に適用する場合は、メソッドを使用する必要がありますcompose。このメソッドは、2 番目の関数を入力に適用してから、最初の関数を 2 番目の関数の出力に適用します。
これは、定義されている方法とは逆の順序で適用する必要がある関数を連鎖させたい場合に便利です。
一般に、andThen定義されている順序で関数を適用する場合に使用し、compose逆の順序で関数を適用する場合に使用する必要があります。ただし、最終的に 2 つの方法のどちらを選択するかは、ユース ケースの特定の要件によって異なります。
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
このcompose例では、関数は右から左の順序で実行されます。まず、multiplyが実行され、その出力が関数に渡されますadd。次に、関数の結果の出力が、ログの目的で にadd渡されますlogOutput。
メソッドapply()は入力を受け取り、結果を返します。関数を引数に適用し、結果を計算するために使用されます。
関数apply()は、関数インターフェイスなどの関数インターフェイスのメソッドであり、指定された型の引数を取り、指定された型の結果を返します。これは、これらのインターフェースの単一の抽象メソッドであり、機能インターフェースとして使用するために必要です。
関数apply()は、機能インターフェースの動作を定義します。機能インターフェースのインスタンスが作成されると、apply()関数が実装され、引数を指定して呼び出されたときに機能インターフェースが何をするかが定義されます。
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
multiplyこのコードでは、整数の引数を取り、その整数の 2 乗を返すという関数を定義します。次に、この関数で関数を呼び出しapply()、整数値 5 を渡します。関数は、関数apply()で定義されたラムダ式を実行しsquare、計算の結果 (この場合は 25) を返します。
実際の例である FileReaderPipeline を見て理解しましょう。
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
上記のコードは、ファイルから行を読み取り、関数のパイプラインを介してそれらを処理し、結果の出力を出力する FileReaderPipeline クラスを定義します。
まず、メソッド参照とラムダ式を使用してtrim、toUpperCase、の 3 つの関数を定義します。文字列から先頭と末尾の空白を削除し、文字列を大文字に変換して、文字列からアルファベット以外の文字をすべて削除します。replaceSpecialCharacterstrimtoUpperCasereplaceSpecialCharacters
次に、メソッドを使用して関数パイプラインが作成されandThen、3 つの関数がチェーンされます。パイプライン関数への入力は文字列で、出力も文字列です。String がパイプライン関数に渡されると、最初に先頭または末尾の空白が削除され、次に文字列が大文字に変換され、最後にアルファベット以外の文字がすべて削除されます。
最後に、mainこのメソッドでは、プログラムはBufferedReader. 各行は、メソッドを使用してパイプライン関数によって処理されますapply。このメソッドは、パイプライン内の各関数を順番に適用し、結果の出力を返します。結果の出力は、 を使用してコンソールに出力されますSystem.out.println。
Java util パッケージには、「Function<A, B>」および「BiFunction<A, B, C>」と呼ばれる 2 つの機能インターフェイスが含まれています。インターフェイスFunctionは 1 つの入力を取り、出力を生成しますが、BiFunctionインターフェイスは 2 つの入力を取り、出力を生成します。以下に図を示します。
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
1 つの入力を受け入れる関数と 2 つの入力を受け入れる関数の 2 つしかないため、これは明らかな制約です。したがって、3 つの入力を受け取る関数が必要な場合は、独自のカスタマイズされた関数を作成する必要があります。
この目的のために、3 つの入力を受け入れる関数を設計して、名前を付けましょうTriFunction。実装例を次に示します。
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
利用可能な関数が受け入れることができるよりも多くのパラメーターが必要な状況では、カリー化を利用してこの制限を克服できます。
カリー化を使用して前の関数をリファクタリングできます。カリー化には、複数の引数を取る関数を、それぞれが 1 つの引数のみを受け入れる関数のシーケンスに分解することが含まれます。これは、関数型プログラミングで採用されている手法であるカリー化の定義と一致しています。
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
上記は、入力を受け取る新しいFunction名前付きを宣言します。入力を受け取り、入力を受け取って出力を返すさらに別の new を返す newを返します。同様に、という名前の新しい関数が作成されます。sumWithThreeParamsIntegerFunctionIntegerFunctionIntegerIntegersumWithFourParams
カリー化された関数と を使用するsumWithThreeParamsとsumWithFourParams、関数に引数を部分的に適用できることに注意してください。
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
この記事では、Java での関数型プログラミング (FP) の概念の実装について説明しました。たとえば、関数を第一級市民として扱い、それらをパイプラインに構成し、カリー化を利用して複雑な関数を単純化するなどです。
java.util.function.Functionやなどの最新の言語機能を使用した例java.util.function.BiFunctionと、その制限を克服するためにカリー化を使用したユーザー定義の例を提供しましたTriFunction。
これらの手法を適用することで、開発者はより簡潔で再利用可能で保守しやすいコードを Java で記述できます。
業界で FP の人気が高まるにつれて、Java 開発者にとってこれらの概念を理解して実装することがますます重要になっています。
この記事を読んだ後、この記事が役に立ち、より実用的な例を調べたい場合は、リポジトリにアクセスして星を付けることを検討してください。
#java #functionalprogramming
1678871340
Apprenez à implémenter des concepts de programmation fonctionnelle (FP) en Java, notamment en visualisant les fonctions comme des citoyens de première classe, en les enchaînant et en les composant pour créer des pipelines de fonctions.
La programmation fonctionnelle (PF) est un paradigme de programmation. Il met l'accent sur l'utilisation de fonctions pures qui n'ont pas d'effets secondaires et renvoient toujours la même sortie pour une entrée donnée.
Cet article explore comment implémenter les concepts FP en Java, y compris la visualisation des fonctions comme des citoyens de première classe, leur chaînage et leur composition pour créer des pipelines de fonctions.
Nous aborderons également la technique du currying, qui permet de transformer une fonction qui prend plusieurs arguments en une chaîne de fonctions qui prennent chacune un seul argument. Cela peut simplifier l'utilisation de fonctions complexes et les rendre plus réutilisables.
Dans cet article, je vais vous montrer des exemples d'implémentation de ces concepts en Java à l'aide de fonctionnalités de langage modernes, telles que "java.util.function.Function", "java.util.function.BiFunction" et une TriFunction définie par l'utilisateur. . Nous verrons ensuite comment dépasser sa limitation en utilisant le currying.
Table des matières:
L'interface java.util.function.Function est un composant clé de l'API de programmation fonctionnelle Java 8. La java.util.function.Function est une interface fonctionnelle en Java qui prend une entrée de type 'T' et produit une sortie de type 'R'.
Dans la programmation fonctionnelle, les fonctions sont des citoyens de première classe, ce qui signifie qu'elles peuvent être transmises en tant que valeurs, stockées dans des variables ou des structures de données, et utilisées comme arguments ou valeurs de retour d'autres fonctions.
L'interface de fonction fournit un moyen de définir et de manipuler des fonctions dans le code Java.
L'interface de fonction représente une fonction qui prend une entrée et produit une sortie. Il a une seule méthode abstraite, apply(), qui prend un argument d'un type spécifié et renvoie un résultat d'un type spécifié.
You can use the function interface to define new functions, as well as to manipulate and compose existing functions. For example, you can use it to convert a list of objects of one type to a list of objects of another type, or to apply a series of transformations to a stream of data.
One of the main benefits of the function interface is that it allows you to write code that is more concise and expressive. By defining functions as values and passing them around as arguments or return values, developers can create more modular and reusable code. Also, by using lambdas to define functions, Java code can be more expressive and easier to read.
You can think about java.util.function.Function like this:
Function<A, B>
The java.util.function.BiFunction is also a functional interface in Java that takes two inputs 'T' and 'U' and produces an output of type 'R'. In short, BiFunction takes 2 inputs and returns an output:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
You can think about java.util.function.BiFunction like this:
BiFunction<A, B, C>
Function chaining is a technique in functional programming that involves composing multiple functions into a single pipeline or chain.
In Java FP, function chaining is often used to transform data in a series of steps, where each step applies a specific transformation to the data and passes it on to the next step in the chain.
The function interface in Java provides a powerful tool for function chaining. Each function in the chain is defined as a separate instance of the Function interface. The output of each function becomes the input to the next function in the chain. This allows for a series of transformations to be applied to the data, one after another, until the final result is produced.
The "andThen()" method is a default method provided by the function interface in Java. This method takes a sequence of two functions and applies them in succession, using the output of the first function as the input to the second function. This chaining of the functions results in a new function that combines the behavior of both functions in a single transformation.
The syntax of andThen looks like this:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
andThen
Nous pouvons décomposer l'exemple en une série d'étapes. Initialement, la fonction "multiplier" sera exécutée et sa sortie transmise en entrée à la fonction "ajouter". Ensuite, la fonction "logOutput" est utilisée pour enregistrer la sortie résultante.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
L'extrait de code ci-dessus présente la création de trois fonctions, à savoir multiplier, ajouter et logOutput.
La fonction multiplyaccepte une entrée entière et produit une sortie entière, tout comme la addfonction. Mais, la logOutputfonction accepte une entrée entière et ne renvoie rien (comme représenté par l'objet Unit, ce qui implique l'absence de valeur).
La executefonction enchaîne les trois fonctions ensemble, où la sortie de multiplier est utilisée comme entrée pour la fonction d'ajout, et la sortie résultante de l'ajout est transmise à la fonction logOutput à des fins de journalisation. L'exemple ci-dessus présente le chaînage de fonctions à l'aide de la andThenméthode par défaut.
Contrairement à la andThenméthode, la composeméthode est une autre méthode par défaut fournie par l'interface de fonction en Java. Il applique la première fonction à la sortie de la deuxième fonction.
Cela signifie que la deuxième fonction est d'abord appliquée à l'entrée, puis la première fonction est appliquée à la sortie de la deuxième fonction. En conséquence, une chaîne de fonctions est créée où la sortie de la deuxième fonction devient l'entrée de la première fonction.
La fonction de composition ressemble à ceci :
default Function<V, R> compose(Function<? super V, ? extends T> before)
composer
Quand utiliser andThenvs composedépend de l'ordre dans lequel vous voulez que les fonctions soient appliquées.
Si vous souhaitez appliquer les fonctions dans l'ordre dans lequel elles sont définies, de gauche à droite, vous devez utiliser la andThenméthode. Cette méthode applique la première fonction à l'entrée, puis applique la deuxième fonction à la sortie de la première fonction.
Ceci est utile lorsque vous souhaitez enchaîner des fonctions qui traitent les données d'entrée dans un ordre spécifique.
En revanche, si vous souhaitez appliquer les fonctions dans l'ordre inverse, de droite à gauche, alors vous devez utiliser la composeméthode. Cette méthode applique la deuxième fonction à l'entrée, puis applique la première fonction à la sortie de la deuxième fonction.
This is useful when you want to chain together functions that need to be applied in the opposite order of the way they are defined.
In general, you should use andThen when you want to apply the functions in the order they are defined, and use compose when you want to apply the functions in the opposite order. But the choice between the two methods ultimately depends on the specific requirements of your use case.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
In the compose example, the functions are executed in a right-to-left order. Firstly, multiply is executed and its output is passed to the add function. Then, the resulting output of the add function is passed to logOutput for logging purposes.
The apply() method takes an input and returns a result. It is used to apply a function to an argument and compute a result.
The apply() function is a method of functional interfaces, such as the function interface, that takes an argument of a specified type and returns a result of a specified type. It is the single abstract method of these interfaces, which is required for them to be used as functional interfaces.
The apply() function defines the behavior of the functional interface. When an instance of a functional interface is created, the apply() function is implemented to define what the functional interface does when it is called with an argument.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
In this code, we define a function called multiply that takes an integer argument and returns the square of that integer. We then call the apply() function on this function, passing in the integer value of 5. The apply() function executes the lambda expression defined in the square Function, and returns the result of the computation, which in this case is 25.
Let's understand by looking at a real-world example, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
The above code defines a FileReaderPipeline class, which reads lines from a file, processes them through a pipeline of functions, and prints the resulting output.
First, three functions trim, toUpperCase, and replaceSpecialCharacters are defined using method references and lambda expressions. trim removes leading and trailing whitespace from a String, toUpperCase converts the string to uppercase, and replaceSpecialCharacters removes any non-alphabetic characters from the string.
Next, a function pipeline is created using the andThen method, which chains the three functions together. The input to the pipeline function is a String, and the output is also a String. When a String is passed to the pipeline function, it first removes any leading or trailing whitespace, then converts the string to uppercase, and finally removes any non-alphabetic characters.
Finally, in the main method, the program reads lines from a file (in this example, "example.txt") using a BufferedReader. Each line is processed through the pipeline function using the apply method, which applies each function in the pipeline in sequence, and returns the resulting output. The resulting output is then printed to the console using System.out.println.
The Java util package contains two functional interfaces known as "Function<A, B>" and "BiFunction<A, B, C>". The Function interface takes a single input and produces an output, whereas the BiFunction interface takes two inputs and produces an output. Here is an illustration:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Il s'agit d'une contrainte claire car il n'y a que deux fonctions, l'une acceptant une seule entrée et l'autre acceptant deux entrées. Par conséquent, si nous avons besoin d'une fonction qui prend trois entrées, nous devons construire notre propre fonction personnalisée.
À cette fin, concevons une fonction qui accepte trois entrées et nommons-la TriFunction. Voici un exemple d'implémentation :
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
Dans les situations où nous avons besoin de plus de paramètres que ce que les fonctions disponibles peuvent accepter, nous pouvons tirer parti de la curry pour surmonter cette limitation.
Nous pouvons refactoriser la fonction précédente en utilisant le currying, qui consiste à décomposer une fonction qui prend plusieurs arguments en une séquence de fonctions, chacune n'acceptant qu'un seul argument. Ceci est conforme à la définition du curry, qui est une technique employée dans la programmation fonctionnelle.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
Ce qui précède déclare un nouveau Functionnommé sumWithThreeParamsqui prend une Integerentrée. Il renvoie un new Functionqui prend une Integerentrée et renvoie encore un autre new Functionqui prend une Integerentrée et renvoie une Integersortie. De même, une nouvelle fonction nommée sumWithFourParamsest créée.
Notez que la fonction curry sumWithThreeParamset sumWithFourParamsnous permet d'appliquer partiellement des arguments à la fonction, ce qui peut être utile dans les situations où nous avons certaines des entrées disponibles mais pas toutes
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
Dans cet article, nous avons couvert l'implémentation de concepts de programmation fonctionnelle (FP) en Java, tels que le traitement des fonctions comme des citoyens de première classe, leur composition en pipelines et l'utilisation du currying pour simplifier des fonctions complexes.
Nous avons fourni des exemples utilisant des fonctionnalités de langage moderne telles que java.util.function.Functionet java.util.function.BiFunction, ainsi qu'un TriFunctioncurry défini par l'utilisateur pour surmonter ses limites.
En appliquant ces techniques, les développeurs peuvent écrire du code plus concis, réutilisable et maintenable en Java.
Avec la popularité croissante de FP dans l'industrie, la compréhension et la mise en œuvre de ces concepts deviennent de plus en plus importantes pour les développeurs Java.
Après avoir lu cet article, si vous l'avez trouvé utile et souhaitez explorer plus d'exemples pratiques, veuillez visiter le référentiel et envisager de lui attribuer une étoile.
Source : https://www.freecodecamp.org
#java #functionalprogramming
1678867686
Aprenda a implementar conceptos de programación funcional (FP) en Java, incluida la visualización de funciones como ciudadanos de primera clase, el encadenamiento y la composición para crear canalizaciones de funciones.
La programación funcional (FP) es un paradigma de programación. Enfatiza el uso de funciones puras que no tienen efectos secundarios y siempre devuelven la misma salida para una entrada determinada.
Este artículo explora cómo implementar conceptos de FP en Java, incluida la visualización de funciones como ciudadanos de primera clase, el encadenamiento y la composición para crear canalizaciones de funciones.
También discutiremos la técnica de curry, que permite que una función que toma múltiples argumentos se transforme en una cadena de funciones que cada una toma un solo argumento. Esto puede simplificar el uso de funciones complejas y hacerlas más reutilizables.
En este artículo, le mostraré ejemplos de cómo implementar estos conceptos en Java utilizando funciones de lenguaje moderno, como "java.util.function.Function", "java.util.function.BiFunction" y una TriFunction definida por el usuario. . Luego veremos cómo superar su limitación usando curry.
Tabla de contenido:
La interfaz java.util.function.Function es un componente clave de la API de programación funcional de Java 8. java.util.function.Function es una interfaz funcional en Java que toma una entrada de tipo 'T' y produce una salida de tipo 'R'.
En la programación funcional, las funciones son ciudadanos de primera clase, lo que significa que pueden transmitirse como valores, almacenarse en variables o estructuras de datos y usarse como argumentos o devolver valores de otras funciones.
La interfaz de función proporciona una forma de definir y manipular funciones en código Java.
La interfaz de función representa una función que toma una entrada y produce una salida. Tiene un único método abstracto, apply()que toma un argumento de un tipo específico y devuelve un resultado de un tipo específico.
Puede usar la interfaz de función para definir nuevas funciones, así como para manipular y componer funciones existentes. Por ejemplo, puede usarlo para convertir una lista de objetos de un tipo en una lista de objetos de otro tipo, o para aplicar una serie de transformaciones a un flujo de datos.
Uno de los principales beneficios de la interfaz de función es que le permite escribir código que es más conciso y expresivo. Al definir funciones como valores y pasarlos como argumentos o valores devueltos, los desarrolladores pueden crear un código más modular y reutilizable. Además, al usar lambdas para definir funciones, el código Java puede ser más expresivo y más fácil de leer.
Puedes pensar java.util.function.Functionasí:
Función<A, B>
java.util.function.BiFunction también es una interfaz funcional en Java que toma dos entradas 'T' y 'U' y produce una salida de tipo 'R'. En resumen, BiFunction toma 2 entradas y devuelve una salida:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Puedes pensar java.util.function.BiFunctionasí:
BiFunción<A, B, C>
El encadenamiento de funciones es una técnica de programación funcional que implica la composición de múltiples funciones en una sola canalización o cadena.
En Java FP, el encadenamiento de funciones se usa a menudo para transformar datos en una serie de pasos, donde cada paso aplica una transformación específica a los datos y la pasa al siguiente paso de la cadena.
La interfaz de funciones en Java proporciona una poderosa herramienta para el encadenamiento de funciones. Cada función en la cadena se define como una instancia separada de la interfaz de función. La salida de cada función se convierte en la entrada de la siguiente función en la cadena. Esto permite aplicar una serie de transformaciones a los datos, una tras otra, hasta producir el resultado final.
El método "andThen()" es un método predeterminado proporcionado por la interfaz de funciones en Java. Este método toma una secuencia de dos funciones y las aplica en sucesión, usando la salida de la primera función como la entrada de la segunda función. Este encadenamiento de las funciones da como resultado una nueva función que combina el comportamiento de ambas funciones en una única transformación.
La sintaxis de andThense ve así:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
y luego
Podemos dividir el ejemplo en una serie de pasos. Inicialmente se ejecutará la función "multiplicar" y su salida pasará como entrada a la función "sumar". Luego, la función "logOutput" se usa para registrar la salida resultante.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
El fragmento de código anterior muestra la creación de tres funciones, a saber, multiplicar, agregar y logOutput.
La función multiplyacepta una entrada de número entero y produce una salida de número entero, al igual que la addfunción. Pero, la logOutputfunción acepta una entrada entera y no devuelve nada (como lo representa el objeto Unidad, lo que implica la ausencia de un valor).
La executefunción encadena las tres funciones juntas, donde la salida de multiplicar se utiliza como entrada para la función de suma, y la salida resultante de suma se pasa a la función logOutput para fines de registro. El ejemplo anterior muestra el encadenamiento de funciones utilizando el andThenmétodo predeterminado.
A diferencia del andThenmétodo, el composemétodo es otro método predeterminado proporcionado por la interfaz de función en Java. Aplica la primera función a la salida de la segunda función.
Esto significa que la segunda función se aplica primero a la entrada y luego la primera función se aplica a la salida de la segunda función. Como resultado, se crea una cadena de funciones donde la salida de la segunda función se convierte en la entrada de la primera función.
La función de redacción se ve así:
default Function<V, R> compose(Function<? super V, ? extends T> before)
componer
Cuándo usar andThenvs composedepende del orden en que desea que se apliquen las funciones.
Si desea aplicar las funciones en el orden en que están definidas, de izquierda a derecha, debe usar el andThenmétodo. Este método aplica la primera función a la entrada y luego aplica la segunda función a la salida de la primera función.
Esto es útil cuando desea encadenar funciones que procesan los datos de entrada en un orden específico.
Por otro lado, si desea aplicar las funciones en el orden inverso, de derecha a izquierda, entonces debe usar el composemétodo. Este método aplica la segunda función a la entrada y luego aplica la primera función a la salida de la segunda función.
Esto es útil cuando desea encadenar funciones que deben aplicarse en el orden opuesto al que están definidas.
En general, debe usar andThencuando desee aplicar las funciones en el orden en que están definidas y composecuando desee aplicar las funciones en el orden opuesto. Pero la elección entre los dos métodos depende en última instancia de los requisitos específicos de su caso de uso.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
En el composeejemplo, las funciones se ejecutan en orden de derecha a izquierda. En primer lugar, multiplyse ejecuta y su salida se pasa a la addfunción. Luego, la salida resultante de la addfunción se pasa logOutputa fines de registro.
El apply()método toma una entrada y devuelve un resultado. Se utiliza para aplicar una función a un argumento y calcular un resultado.
La apply()función es un método de interfaces funcionales, como la interfaz de función, que toma un argumento de un tipo específico y devuelve un resultado de un tipo específico. Es el método abstracto único de estas interfaces, lo que se requiere para que se utilicen como interfaces funcionales.
La apply()función define el comportamiento de la interfaz funcional. Cuando se crea una instancia de una interfaz funcional, la apply()función se implementa para definir qué hace la interfaz funcional cuando se llama con un argumento.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
En este código, definimos una función llamada multiplyque toma un argumento entero y devuelve el cuadrado de ese entero. Luego llamamos a la apply()función en esta función, pasando el valor entero de 5. La apply()función ejecuta la expresión lambda definida en la squarefunción y devuelve el resultado del cálculo, que en este caso es 25.
Entendamos mirando un ejemplo del mundo real, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
El código anterior define una clase FileReaderPipeline, que lee líneas de un archivo, las procesa a través de una canalización de funciones e imprime la salida resultante.
En primer lugar, se definen tres funciones trim, toUpperCasey mediante referencias a métodos y expresiones lambda. elimina los espacios en blanco iniciales y finales de una cadena, convierte la cadena a mayúsculas y elimina cualquier carácter no alfabético de la cadena.replaceSpecialCharacterstrimtoUpperCasereplaceSpecialCharacters
A continuación, se crea una canalización de función utilizando el andThenmétodo, que encadena las tres funciones juntas. La entrada a la función de canalización es una cadena y la salida también es una cadena. Cuando se pasa una cadena a la función de canalización, primero elimina los espacios en blanco iniciales o finales, luego convierte la cadena a mayúsculas y finalmente elimina los caracteres no alfabéticos.
Finalmente, en el mainmétodo, el programa lee líneas de un archivo (en este ejemplo, "example.txt") usando un archivo BufferedReader. Cada línea se procesa a través de la función de canalización utilizando el applymétodo, que aplica cada función en la canalización en secuencia y devuelve la salida resultante. La salida resultante luego se imprime en la consola usando System.out.println.
El paquete de utilidades de Java contiene dos interfaces funcionales conocidas como "Función<A, B>" y "BiFunción<A, B, C>". La Functioninterfaz toma una sola entrada y produce una salida, mientras que la BiFunctioninterfaz toma dos entradas y produce una salida. Aquí hay una ilustración:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Esta es una restricción clara ya que solo hay dos funciones, una acepta una sola entrada y la otra acepta dos entradas. En consecuencia, si necesitamos una función que tome tres entradas, debemos construir nuestra propia función personalizada.
Con este fin, diseñemos una función que acepte tres entradas y asígnele el nombre TriFunction. Aquí hay una implementación de ejemplo:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
En situaciones en las que necesitamos más parámetros de los que pueden aceptar las funciones disponibles, podemos aprovechar el curry para superar esta limitación.
Podemos refactorizar la función anterior usando currying, lo que implica descomponer una función que toma múltiples argumentos en una secuencia de funciones, cada una de las cuales acepta solo un argumento. Esto está en línea con la definición de curry, que es una técnica empleada en la programación funcional.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
Lo anterior declara un nuevo Functionnamed sumWithThreeParamsque toma una Integerentrada. Devuelve un nuevo Functionque toma una Integerentrada y devuelve otro nuevo Functionque toma una Integerentrada y devuelve una Integersalida. sumWithFourParamsDe manera similar, se crea una nueva función llamada .
Tenga en cuenta que la función curry nos sumWithThreeParamspermite sumWithFourParamsaplicar parcialmente argumentos a la función, lo que puede ser útil en situaciones en las que tenemos algunas de las entradas disponibles pero no todas.
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
En este artículo, cubrimos la implementación de conceptos de programación funcional (FP) en Java, como el tratamiento de funciones como ciudadanos de primera clase, su composición en canalizaciones y el uso de curry para simplificar funciones complejas.
Hemos proporcionado ejemplos que utilizan funciones de lenguaje moderno como java.util.function.Functiony java.util.function.BiFunction, así como un usuario definido TriFunctioncon curry para superar sus limitaciones.
Al aplicar estas técnicas, los desarrolladores pueden escribir código más conciso, reutilizable y mantenible en Java.
Con la creciente popularidad de FP en la industria, comprender e implementar estos conceptos es cada vez más importante para los desarrolladores de Java.
Después de leer este artículo, si lo ha encontrado útil y desea explorar más ejemplos prácticos, visite el repositorio y considere darle una estrella.
Fuente: https://www.freecodecamp.org
#java #functionalprogramming
1678860445
เรียนรู้วิธีนำแนวคิด Functional Programming (FP) ไปใช้ใน Java รวมถึงการดูฟังก์ชันในฐานะพลเมืองชั้นหนึ่ง การผูกมัด และการเขียนเพื่อสร้างฟังก์ชันไปป์ไลน์
Functional Programming (FP) เป็นกระบวนทัศน์การเขียนโปรแกรม เน้นการใช้ฟังก์ชันบริสุทธิ์ที่ไม่มีผลข้างเคียงและส่งคืนเอาต์พุตเดียวกันเสมอสำหรับอินพุตที่กำหนด
บทความนี้จะสำรวจวิธีนำแนวคิด FP ไปใช้ใน Java รวมถึงการดูฟังก์ชันในฐานะพลเมืองชั้นหนึ่ง การผูกมัด และการเขียนเพื่อสร้างฟังก์ชันไปป์ไลน์
นอกจากนี้ เรายังจะหารือเกี่ยวกับเทคนิคการแกง ซึ่งช่วยให้ฟังก์ชันที่รับอาร์กิวเมนต์หลายตัวถูกแปลงเป็นสายของฟังก์ชันที่แต่ละฟังก์ชันรับอาร์กิวเมนต์เดียว สิ่งนี้สามารถลดความซับซ้อนของการใช้ฟังก์ชันที่ซับซ้อนและทำให้สามารถนำมาใช้ซ้ำได้มากขึ้น
ในบทความนี้ ผมจะแสดงตัวอย่างการนำแนวคิดเหล่านี้ไปใช้ใน Java โดยใช้ฟีเจอร์ภาษาสมัยใหม่ เช่น “java.util.function.Function”, “java.util.function.BiFunction” และ TriFunction ที่ผู้ใช้กำหนด . จากนั้นเราจะมาดูวิธีเอาชนะข้อจำกัดโดยใช้การแกง
สารบัญ:
อินเทอร์เฟซ java.util.function.Function เป็นองค์ประกอบหลักของ API โปรแกรมทำงานเชิงฟังก์ชัน Java 8 java.util.function.Function เป็นอินเทอร์เฟซการทำงานใน Java ที่รับอินพุตประเภท 'T' และสร้างเอาต์พุตประเภท 'R'
ในการเขียนโปรแกรมเชิงฟังก์ชัน ฟังก์ชันเป็นพลเมืองชั้นหนึ่ง หมายความว่าสามารถส่งผ่านเป็นค่าต่างๆ เก็บไว้ในตัวแปรหรือโครงสร้างข้อมูล และใช้เป็นอาร์กิวเมนต์หรือส่งคืนค่าของฟังก์ชันอื่นๆ
อินเทอร์เฟซของฟังก์ชันจัดเตรียมวิธีการกำหนดและจัดการฟังก์ชันในโค้ด Java
อินเทอร์เฟซของฟังก์ชันแสดงถึงฟังก์ชันที่รับอินพุตหนึ่งตัวและสร้างเอาต์พุตหนึ่งตัว มีเมธอดนามธรรมวิธีเดียวapply()ซึ่งรับอาร์กิวเมนต์ของประเภทที่ระบุและส่งคืนผลลัพธ์ของประเภทที่ระบุ
You can use the function interface to define new functions, as well as to manipulate and compose existing functions. For example, you can use it to convert a list of objects of one type to a list of objects of another type, or to apply a series of transformations to a stream of data.
One of the main benefits of the function interface is that it allows you to write code that is more concise and expressive. By defining functions as values and passing them around as arguments or return values, developers can create more modular and reusable code. Also, by using lambdas to define functions, Java code can be more expressive and easier to read.
You can think about java.util.function.Function like this:
Function<A, B>
The java.util.function.BiFunction is also a functional interface in Java that takes two inputs 'T' and 'U' and produces an output of type 'R'. In short, BiFunction takes 2 inputs and returns an output:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
You can think about java.util.function.BiFunction like this:
BiFunction<A, B, C>
Function chaining is a technique in functional programming that involves composing multiple functions into a single pipeline or chain.
In Java FP, function chaining is often used to transform data in a series of steps, where each step applies a specific transformation to the data and passes it on to the next step in the chain.
The function interface in Java provides a powerful tool for function chaining. Each function in the chain is defined as a separate instance of the Function interface. The output of each function becomes the input to the next function in the chain. This allows for a series of transformations to be applied to the data, one after another, until the final result is produced.
The "andThen()" method is a default method provided by the function interface in Java. This method takes a sequence of two functions and applies them in succession, using the output of the first function as the input to the second function. This chaining of the functions results in a new function that combines the behavior of both functions in a single transformation.
The syntax of andThen looks like this:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
andThen
เราสามารถแบ่งตัวอย่างออกเป็นขั้นตอนต่างๆ ในขั้นต้น ฟังก์ชัน "คูณ" จะถูกดำเนินการ และเอาต์พุตจะถูกส่งผ่านเป็นอินพุตไปยังฟังก์ชัน "เพิ่ม" จากนั้นใช้ฟังก์ชัน "logOutput" เพื่อบันทึกผลลัพธ์ที่ได้
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
ข้อมูลโค้ดด้านบนแสดงการสร้างฟังก์ชันสามฟังก์ชัน ได้แก่ คูณ เพิ่ม และล็อกเอาท์พุต
ฟังก์ชันmultiplyจะรับอินพุตเป็นจำนวนเต็มและสร้างเอาต์พุตเป็นจำนวนเต็ม เช่นเดียวกับaddฟังก์ชัน แต่logOutputฟังก์ชันจะยอมรับอินพุตที่เป็นจำนวนเต็มและไม่ส่งคืนสิ่งใดเลย (ตามที่แสดงโดยวัตถุ Unit ซึ่งแสดงถึงการไม่มีค่า)
ฟังก์ชันexecuteเชนเชื่อมโยงฟังก์ชันทั้งสามเข้าด้วยกัน โดยที่เอาต์พุตของการคูณถูกใช้เป็นอินพุตสำหรับฟังก์ชันเพิ่ม และเอาต์พุตที่เป็นผลลัพธ์ของการเพิ่มจะถูกส่งผ่านไปยังฟังก์ชัน logOutput เพื่อวัตถุประสงค์ในการบันทึก ตัวอย่างข้างต้นแสดงการผูกมัดของฟังก์ชันโดยใช้andThenวิธีการเริ่มต้น
ซึ่งแตกต่างจากandThenเมธอด เมธอดcomposeเป็นอีกเมธอดดีฟอลต์ที่จัดเตรียมโดยอินเทอร์เฟซของฟังก์ชันใน Java ใช้ฟังก์ชันแรกกับเอาต์พุตของฟังก์ชันที่สอง
ซึ่งหมายความว่าฟังก์ชันที่สองจะถูกนำไปใช้กับอินพุตก่อน จากนั้นฟังก์ชันแรกจะถูกนำไปใช้กับเอาต์พุตของฟังก์ชันที่สอง เป็นผลให้มีการสร้างห่วงโซ่ของฟังก์ชันโดยที่เอาต์พุตของฟังก์ชันที่สองกลายเป็นอินพุตของฟังก์ชันแรก
ฟังก์ชันการเขียนมีลักษณะดังนี้:
default Function<V, R> compose(Function<? super V, ? extends T> before)
เขียน
เมื่อใดที่จะใช้andThenvs composeขึ้นอยู่กับลำดับที่คุณต้องการให้นำฟังก์ชันไปใช้
หากคุณต้องการใช้ฟังก์ชันตามลำดับที่กำหนดไว้ จากซ้ายไปขวา คุณควรใช้andThenเมธอด วิธีนี้ใช้ฟังก์ชันแรกกับอินพุต จากนั้นใช้ฟังก์ชันที่สองกับเอาต์พุตของฟังก์ชันแรก
สิ่งนี้มีประโยชน์เมื่อคุณต้องการเชื่อมโยงฟังก์ชันที่ประมวลผลข้อมูลอินพุตตามลำดับที่ระบุเข้าด้วยกัน
ในทางกลับกัน หากคุณต้องการใช้ฟังก์ชันในลำดับตรงข้าม จากขวาไปซ้าย คุณควรใช้composeเมธอด วิธีนี้ใช้ฟังก์ชันที่สองกับอินพุต จากนั้นใช้ฟังก์ชันแรกกับเอาต์พุตของฟังก์ชันที่สอง
สิ่งนี้มีประโยชน์เมื่อคุณต้องการรวมฟังก์ชันที่ต้องใช้ในลำดับที่ตรงกันข้ามกับวิธีที่กำหนดไว้
โดยทั่วไป คุณควรใช้andThenเมื่อต้องการใช้ฟังก์ชันตามลำดับที่กำหนดไว้ และใช้composeเมื่อต้องการใช้ฟังก์ชันในลำดับตรงข้าม แต่ตัวเลือกระหว่างสองวิธีนั้นขึ้นอยู่กับข้อกำหนดเฉพาะของกรณีการใช้งานของคุณ
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
ในcomposeตัวอย่าง ฟังก์ชันจะดำเนินการตามลำดับจากขวาไปซ้าย ประการแรกmultiplyดำเนินการและส่งออกไปยังaddฟังก์ชัน จากนั้น เอาต์พุตที่เป็นผลลัพธ์ของaddฟังก์ชันจะถูกส่งผ่านlogOutputเพื่อวัตถุประสงค์ในการบันทึก
เมธอดapply()รับอินพุตและส่งกลับผลลัพธ์ ใช้เพื่อนำฟังก์ชันไปใช้กับอาร์กิวเมนต์และคำนวณผลลัพธ์
ฟังก์ชันapply()คือเมธอดของอินเทอร์เฟซการทำงาน เช่น อินเทอร์เฟซของฟังก์ชัน ซึ่งรับอาร์กิวเมนต์ของประเภทที่ระบุและส่งคืนผลลัพธ์ของประเภทที่ระบุ เป็นวิธีการเชิงนามธรรมเดียวของอินเทอร์เฟซเหล่านี้ ซึ่งจำเป็นสำหรับใช้เป็นอินเทอร์เฟซการทำงาน
ฟังก์ชันapply()กำหนดพฤติกรรมของอินเทอร์เฟซการทำงาน เมื่อมีการสร้างอินสแตนซ์ของส่วนต่อประสานการทำงานapply()ฟังก์ชันจะถูกนำไปใช้เพื่อกำหนดว่าส่วนต่อประสานการทำงานนั้นทำอะไรเมื่อถูกเรียกด้วยอาร์กิวเมนต์
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
ในโค้ดนี้ เรากำหนดฟังก์ชันที่เรียกmultiplyว่ารับอาร์กิวเมนต์จำนวนเต็มและส่งกลับกำลังสองของจำนวนเต็มนั้น จากนั้นเราจะเรียกใช้apply()ฟังก์ชันบนฟังก์ชันนี้ โดยส่งผ่านค่าจำนวนเต็มเป็น 5 ฟังก์ชันapply()จะดำเนินการตามนิพจน์แลมบ์ดาที่กำหนดไว้ในsquareฟังก์ชัน และส่งกลับผลลัพธ์ของการคำนวณ ซึ่งในกรณีนี้คือ 25
มาทำความเข้าใจโดยดูตัวอย่างในโลกแห่งความเป็นจริง FileReaderPipeline
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
โค้ดข้างต้นกำหนดคลาส FileReaderPipeline ซึ่งอ่านบรรทัดจากไฟล์ ประมวลผลผ่านไปป์ไลน์ของฟังก์ชัน และพิมพ์ผลลัพธ์ที่ได้
First, three functions trim, toUpperCase, and replaceSpecialCharacters are defined using method references and lambda expressions. trim removes leading and trailing whitespace from a String, toUpperCase converts the string to uppercase, and replaceSpecialCharacters removes any non-alphabetic characters from the string.
Next, a function pipeline is created using the andThen method, which chains the three functions together. The input to the pipeline function is a String, and the output is also a String. When a String is passed to the pipeline function, it first removes any leading or trailing whitespace, then converts the string to uppercase, and finally removes any non-alphabetic characters.
Finally, in the main method, the program reads lines from a file (in this example, "example.txt") using a BufferedReader. Each line is processed through the pipeline function using the apply method, which applies each function in the pipeline in sequence, and returns the resulting output. The resulting output is then printed to the console using System.out.println.
The Java util package contains two functional interfaces known as "Function<A, B>" and "BiFunction<A, B, C>". The Function interface takes a single input and produces an output, whereas the BiFunction interface takes two inputs and produces an output. Here is an illustration:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
This is a clear constraint as there are only two functions, with one accepting a single input and the other accepting two inputs. Consequently, if we need a function that takes three inputs, we must construct our own customized function.
To this end, let's design a function that accepts three inputs and name it TriFunction. Here is an example implementation:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
In situations where we require more parameters than what the available functions can accept, we can leverage currying to overcome this limitation.
We can refactor the previous function using currying, which involves decomposing a function that takes multiple arguments into a sequence of functions, each of which accepts only one argument. This is in line with the definition of currying, which is a technique employed in Functional Programming.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
ด้านบนประกาศFunctionชื่อ ใหม่ sumWithThreeParamsที่รับIntegerอินพุต มันส่งกลับใหม่Functionที่รับIntegerอินพุตและส่งคืนอีกอันใหม่Functionที่รับIntegerอินพุตและส่งคืนIntegerเอาต์พุต ในทำนองเดียวกันฟังก์ชันใหม่ชื่อsumWithFourParamsจะถูกสร้างขึ้น
โปรดทราบว่าฟังก์ชัน curried sumWithThreeParamsและsumWithFourParamsอนุญาตให้เรานำอาร์กิวเมนต์ไปใช้กับฟังก์ชันได้บางส่วน ซึ่งจะมีประโยชน์ในสถานการณ์ที่เรามีอินพุตบางส่วนที่พร้อมใช้งาน แต่ไม่ใช่ทั้งหมด
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
ในบทความนี้ เรากล่าวถึงการนำแนวคิดการเขียนโปรแกรมเชิงฟังก์ชัน (FP) ไปใช้ใน Java เช่น การปฏิบัติต่อฟังก์ชันในฐานะพลเมืองชั้นหนึ่ง การจัดองค์ประกอบลงในไปป์ไลน์ และการใช้การแกงเพื่อลดความซับซ้อนของฟังก์ชันที่ซับซ้อน
เราได้ให้ตัวอย่างการใช้คุณสมบัติภาษาสมัยใหม่ เช่นjava.util.function.Functionและjava.util.function.BiFunctionเช่นเดียวกับที่ผู้ใช้กำหนดTriFunctionด้วย currying เพื่อเอาชนะข้อจำกัด
ด้วยการใช้เทคนิคเหล่านี้ นักพัฒนาสามารถเขียนโค้ดที่กระชับ ใช้ซ้ำได้ และบำรุงรักษาได้ใน Java
ด้วยความนิยมที่เพิ่มขึ้นของ FP ในอุตสาหกรรม การทำความเข้าใจและนำแนวคิดเหล่านี้ไปใช้จึงมีความสำคัญมากขึ้นสำหรับนักพัฒนา Java
หลังจากอ่านบทความนี้แล้ว หากคุณพบว่ามีประโยชน์และต้องการสำรวจตัวอย่างที่ใช้งานได้จริงเพิ่มเติมโปรดไปที่ที่เก็บและให้ดาวแก่บทความนี้
ที่มา: https://www.freecodecamp.org
#java #functionalprogramming
1678856820
了解如何在 Java 中實現函數式編程 (FP) 概念,包括將函數視為一等公民、鏈接和組合它們以創建函數管道。
函數式編程(FP)是一種編程範式。它強調使用沒有副作用的純函數,並且總是為給定的輸入返回相同的輸出。
本文探討如何在 Java 中實現 FP 概念,包括將函數視為一等公民、鏈接和組合它們以創建函數管道。
我們還將討論柯里化技術,它允許將帶有多個參數的函數轉換為一個函數鏈,每個函數帶有一個參數。這可以簡化複雜功能的使用並使它們更易於重用。
在本文中,我將向您展示如何使用現代語言功能在 Java 中實現這些概念的示例,例如“java.util.function.Function”、“java.util.function.BiFunction”和用戶定義的 TriFunction . 然後我們將看到如何使用柯里化來克服它的局限性。
目錄:
java.util.function.Function 接口是 Java 8 函數式編程 API 的關鍵組件。java.util.function.Function 是 Java 中的函數式接口,它接受“T”類型的輸入並產生“R”類型的輸出。
在函數式編程中,函數是一等公民,這意味著它們可以作為值傳遞,存儲在變量或數據結構中,並用作其他函數的參數或返回值。
函數接口提供了一種在 Java 代碼中定義和操作函數的方法。
函數接口表示接受一個輸入並產生一個輸出的函數。它有一個抽象方法,apply()它接受指定類型的參數並返回指定類型的結果。
您可以使用函數接口來定義新函數,以及操作和組合現有函數。例如,您可以使用它將一種類型的對象列表轉換為另一種類型的對象列表,或者將一系列轉換應用於數據流。
函數接口的主要好處之一是它允許您編寫更簡潔和更具表現力的代碼。通過將函數定義為值並將它們作為參數或返回值傳遞,開發人員可以創建更加模塊化和可重用的代碼。此外,通過使用 lambda 表達式來定義函數,Java 代碼可以更具表現力並且更易於閱讀。
java.util.function.Function你可以這樣想:
函數<A, B>
java.util.function.BiFunction 也是 Java 中的函數接口,它接受兩個輸入“T”和“U”並產生類型“R”的輸出。簡而言之,BiFunction 接受 2 個輸入並返回一個輸出:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
java.util.function.BiFunction你可以這樣想:
雙函數<A, B, C>
函數鏈接是函數式編程中的一種技術,涉及將多個函數組合到單個管道或鏈中。
在 Java FP 中,函數鏈通常用於在一系列步驟中轉換數據,其中每個步驟對數據應用特定的轉換並將其傳遞到鏈中的下一步。
Java 中的函數接口為函數鏈接提供了強大的工具。鏈中的每個函數都定義為 Function 接口的一個單獨實例。每個函數的輸出成為鏈中下一個函數的輸入。這允許對數據應用一系列轉換,一個接一個,直到產生最終結果。
“andThen()”方法是Java中函數接口提供的默認方法。此方法採用兩個函數的序列並連續應用它們,使用第一個函數的輸出作為第二個函數的輸入。這種函數鏈接產生了一個新函數,它將兩個函數的行為組合在一個轉換中。
的語法andThen如下所示:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
進而
我們可以將示例分解為一系列步驟。最初將執行“乘法”函數,並將其輸出作為輸入傳遞給“加法”函數。然後“logOutput”函數用於記錄結果輸出。
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
上面的代碼片段展示了三個函數的創建,即 multiply、add 和 logOutput。
該函數multiply接受一個整數輸入並產生一個整數輸出,就像add函數一樣。但是,該logOutput函數接受整數輸入並且不返回任何內容(由 Unit 對象表示,這意味著沒有值)。
該execute函數將三個函數鏈接在一起,其中 multiply 的輸出用作 add 函數的輸入,add 的結果輸出傳遞給 logOutput 函數以進行日誌記錄。上面的示例展示了使用andThen默認方法的函數鏈接。
與method不同andThen,composemethod是Java中函數接口提供的另一種默認方法。它將第一個函數應用於第二個函數的輸出。
這意味著第二個函數首先應用於輸入,然後第一個函數應用於第二個函數的輸出。結果,創建了一個函數鏈,其中第二個函數的輸出成為第一個函數的輸入。
撰寫功能如下所示:
default Function<V, R> compose(Function<? super V, ? extends T> before)
撰寫
何時使用andThenvscompose取決於您希望應用函數的順序。
如果你想按照定義的順序應用函數,從左到右,那麼你應該使用該andThen方法。此方法將第一個函數應用於輸入,然後將第二個函數應用於第一個函數的輸出。
當您想要將以特定順序處理輸入數據的函數鏈接在一起時,這很有用。
另一方面,如果你想以相反的順序應用函數,從右到左,那麼你應該使用方法compose。此方法將第二個函數應用於輸入,然後將第一個函數應用於第二個函數的輸出。
當您想要將需要以與定義方式相反的順序應用的函數鏈接在一起時,這很有用。
一般來說,andThen當你想按照定義的順序應用函數時應該使用,而compose當你想以相反的順序應用函數時應該使用。但是兩種方法之間的選擇最終取決於您的用例的具體要求。
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
在compose示例中,函數按從右到左的順序執行。首先,multiply執行並將其輸出傳遞給函數add。然後,函數的結果輸出add被傳遞logOutput給日誌記錄目的。
該apply()方法接受輸入並返回結果。它用於將函數應用於參數併計算結果。
函數apply()是函數接口的一種方法,例如函數接口,它接受指定類型的參數並返回指定類型的結果。它是這些接口的單一抽象方法,這是將它們用作功能接口所必需的。
該apply()函數定義了功能接口的行為。創建功能接口的實例時,apply()將實現該功能以定義功能接口在使用參數調用時執行的操作。
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
在這段代碼中,我們定義了一個名為的函數multiply,它接受一個整數參數並返回該整數的平方。然後我們apply()在這個函數上調用函數,傳入整數值 5。該apply()函數執行 Function 中定義的 lambda 表達式square,並返回計算結果,在本例中為 25。
讓我們通過看一個真實世界的例子來理解,FileReaderPipeline。
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
上面的代碼定義了一個 FileReaderPipeline 類,它從文件中讀取行,通過函數管道處理它們,並打印結果輸出。
首先,使用方法引用和 lambda 表達式定義了三個函數trim,toUpperCase和。從字符串中刪除前導和尾隨空格,將字符串轉換為大寫,並從字符串中刪除任何非字母字符。replaceSpecialCharacterstrimtoUpperCasereplaceSpecialCharacters
andThen接下來,使用將三個函數鏈接在一起的方法創建函數管道。管道函數的輸入是一個字符串,輸出也是一個字符串。當一個 String 被傳遞給管道函數時,它首先刪除任何前導或尾隨的空格,然後將字符串轉換為大寫,最後刪除任何非字母字符。
最後,在main該方法中,程序使用BufferedReader. 每行都使用該方法通過管道函數進行處理apply,該方法按順序應用管道中的每個函數,並返回結果輸出。然後使用 將結果輸出打印到控制台System.out.println。
Java util 包包含兩個功能接口,稱為“Function<A, B>”和“BiFunction<A, B, C>”。接口Function採用單個輸入並產生輸出,而BiFunction接口採用兩個輸入並產生輸出。這是一個例子:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
這是一個明確的約束,因為只有兩個函數,一個接受單個輸入,另一個接受兩個輸入。因此,如果我們需要一個接受三個輸入的函數,我們必須構建我們自己的自定義函數。
為此,讓我們設計一個接受三個輸入的函數並將其命名為TriFunction。這是一個示例實現:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
在我們需要的參數多於可用函數可以接受的參數的情況下,我們可以利用柯里化來克服這個限制。
我們可以使用 currying 重構前面的函數,這涉及將一個接受多個參數的函數分解為一系列函數,每個函數只接受一個參數。這符合柯里化的定義,柯里化是函數式編程中採用的一種技術。
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
上面聲明了一個接受輸入的新Function命名。它返回一個接受輸入並返回另一個接受輸入並返回輸出的 new。同樣,創建了一個名為的新函數。sumWithThreeParamsIntegerFunctionIntegerFunctionIntegerIntegersumWithFourParams
請注意,curried 函數sumWithThreeParamsandsumWithFourParams允許我們將部分參數應用於函數,這在我們有一些輸入可用但不是全部可用的情況下很有用
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
在本文中,我們介紹了 Java 中函數式編程 (FP) 概念的實現,例如將函數視為一等公民,將它們組合到管道中,以及利用柯里化來簡化複雜函數。
我們提供了使用java.util.function.Function和 等現代語言功能的示例java.util.function.BiFunction,以及使用TriFunction柯里化來克服其局限性的用戶定義。
通過應用這些技術,開發人員可以用 Java 編寫更簡潔、可重用和可維護的代碼。
隨著 FP 在業界的日益普及,理解和實現這些概念對於 Java 開發人員來說變得越來越重要。
閱讀本文後,如果您覺得它有用並想探索更多實用示例,請訪問存儲庫並考慮給它一個星。
資料來源: https: //www.freecodecamp.org
#java #functionalprogramming
1678853164
Scopri come implementare i concetti di programmazione funzionale (FP) in Java, inclusa la visualizzazione delle funzioni come cittadini di prima classe, il concatenamento e la composizione per creare pipeline di funzioni.
La programmazione funzionale (FP) è un paradigma di programmazione. Sottolinea l'uso di funzioni pure che non hanno effetti collaterali e restituiscono sempre lo stesso output per un dato input.
Questo articolo esplora come implementare i concetti FP in Java, inclusa la visualizzazione delle funzioni come cittadini di prima classe, il concatenamento e la composizione per creare pipeline di funzioni.
Discuteremo anche della tecnica del currying, che consente di trasformare una funzione che accetta più argomenti in una catena di funzioni che accettano ciascuna un singolo argomento. Questo può semplificare l'uso di funzioni complesse e renderle più riutilizzabili.
In questo articolo, ti mostrerò esempi di come implementare questi concetti in Java utilizzando le funzionalità del linguaggio moderno, come "java.util.function.Function", "java.util.function.BiFunction" e una TriFunction definita dall'utente . Vedremo poi come superare il suo limite utilizzando il currying.
Sommario:
The java.util.function.Function interface is a key component of the Java 8 functional programming API. The java.util.function.Function is a functional interface in Java that takes input of type 'T' and produces an output of type 'R'.
In functional programming, functions are first-class citizens, meaning that they can be passed around as values, stored in variables or data structures, and used as arguments or return values of other functions.
The function interface provides a way to define and manipulate functions in Java code.
The function interface represents a function that takes one input and produces one output. It has a single abstract method, apply(), which takes an argument of a specified type and returns a result of a specified type.
You can use the function interface to define new functions, as well as to manipulate and compose existing functions. For example, you can use it to convert a list of objects of one type to a list of objects of another type, or to apply a series of transformations to a stream of data.
One of the main benefits of the function interface is that it allows you to write code that is more concise and expressive. By defining functions as values and passing them around as arguments or return values, developers can create more modular and reusable code. Also, by using lambdas to define functions, Java code can be more expressive and easier to read.
You can think about java.util.function.Function like this:
Function<A, B>
The java.util.function.BiFunction is also a functional interface in Java that takes two inputs 'T' and 'U' and produces an output of type 'R'. In short, BiFunction takes 2 inputs and returns an output:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
You can think about java.util.function.BiFunction like this:
BiFunction<A, B, C>
Function chaining is a technique in functional programming that involves composing multiple functions into a single pipeline or chain.
In Java FP, function chaining is often used to transform data in a series of steps, where each step applies a specific transformation to the data and passes it on to the next step in the chain.
The function interface in Java provides a powerful tool for function chaining. Each function in the chain is defined as a separate instance of the Function interface. The output of each function becomes the input to the next function in the chain. This allows for a series of transformations to be applied to the data, one after another, until the final result is produced.
The "andThen()" method is a default method provided by the function interface in Java. This method takes a sequence of two functions and applies them in succession, using the output of the first function as the input to the second function. This chaining of the functions results in a new function that combines the behavior of both functions in a single transformation.
The syntax of andThen looks like this:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
andThen
We can break the example down into series of steps. Initially the "multiply" function will be executed and its output passed as input to the "add" function. Then the "logOutput" function is used to log the resulting output.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
The the above code snippet showcases the creation of three functions, namely multiply, add, and logOutput.
The function multiply accepts an integer input and produces an integer output, just like the add function. But, the logOutput function accepts an integer input and returns nothing (as represented by the Unit object, which implies the absence of a value).
La executefunzione concatena le tre funzioni insieme, in cui l'output di multiply viene utilizzato come input per la funzione add e l'output risultante di add viene passato alla funzione logOutput per scopi di registrazione. L'esempio precedente mostra il concatenamento delle funzioni utilizzando il andThenmetodo predefinito.
A differenza del andThenmetodo, il composemetodo è un altro metodo predefinito fornito dall'interfaccia della funzione in Java. Applica la prima funzione all'output della seconda funzione.
Ciò significa che la seconda funzione viene applicata prima all'input, quindi la prima funzione viene applicata all'output della seconda funzione. Di conseguenza, viene creata una catena di funzioni in cui l'output della seconda funzione diventa l'input della prima funzione.
La funzione di composizione è simile a questa:
default Function<V, R> compose(Function<? super V, ? extends T> before)
comporre
Quando utilizzare andThenvs composedipende dall'ordine in cui si desidera applicare le funzioni.
Se si desidera applicare le funzioni nell'ordine in cui sono definite, da sinistra a destra, è necessario utilizzare il andThenmetodo. Questo metodo applica la prima funzione all'input, quindi applica la seconda funzione all'output della prima funzione.
Ciò è utile quando si desidera concatenare funzioni che elaborano i dati di input in un ordine specifico.
Se invece vuoi applicare le funzioni nell'ordine inverso, da destra a sinistra, allora dovresti usare il composemetodo. Questo metodo applica la seconda funzione all'input, quindi applica la prima funzione all'output della seconda funzione.
Ciò è utile quando si desidera concatenare funzioni che devono essere applicate nell'ordine opposto rispetto a come sono definite.
In generale, si dovrebbe utilizzare andThenquando si desidera applicare le funzioni nell'ordine in cui sono definite e utilizzare composequando si desidera applicare le funzioni nell'ordine opposto. Ma la scelta tra i due metodi dipende in ultima analisi dai requisiti specifici del tuo caso d'uso.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
Nell'esempio compose, le funzioni vengono eseguite in ordine da destra a sinistra. In primo luogo, multiplyviene eseguito e il suo output viene passato alla addfunzione. Quindi, l'output risultante della addfunzione viene passato a logOutputper scopi di registrazione.
Il apply()metodo accetta un input e restituisce un risultato. Viene utilizzato per applicare una funzione a un argomento e calcolare un risultato.
La apply()funzione è un metodo di interfacce funzionali, come l'interfaccia della funzione, che accetta un argomento di un tipo specificato e restituisce un risultato di un tipo specificato. È l'unico metodo astratto di queste interfacce, necessario per essere utilizzate come interfacce funzionali.
La apply()funzione definisce il comportamento dell'interfaccia funzionale. Quando viene creata un'istanza di un'interfaccia funzionale, la apply()funzione viene implementata per definire cosa fa l'interfaccia funzionale quando viene chiamata con un argomento.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
In questo codice, definiamo una funzione chiamata multiplyche accetta un argomento intero e restituisce il quadrato di quell'intero. Quindi chiamiamo la apply()funzione su questa funzione, passando il valore intero di 5. La apply()funzione esegue l'espressione lambda definita nella squarefunzione e restituisce il risultato del calcolo, che in questo caso è 25.
Comprendiamo guardando un esempio del mondo reale, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
Il codice precedente definisce una classe FileReaderPipeline, che legge le righe da un file, le elabora attraverso una pipeline di funzioni e stampa l'output risultante.
In primo luogo, tre funzioni trim, toUpperCasee replaceSpecialCharactersvengono definite utilizzando riferimenti ai metodi ed espressioni lambda. trimrimuove gli spazi bianchi iniziali e finali da una stringa, toUpperCaseconverte la stringa in maiuscolo e replaceSpecialCharactersrimuove eventuali caratteri non alfabetici dalla stringa.
Successivamente, viene creata una pipeline di funzioni utilizzando il andThenmetodo, che concatena insieme le tre funzioni. L'input per la funzione pipeline è una stringa e anche l'output è una stringa. Quando una stringa viene passata alla funzione pipeline, prima rimuove qualsiasi spazio bianco iniziale o finale, quindi converte la stringa in maiuscolo e infine rimuove tutti i caratteri non alfabetici.
Infine, nel mainmetodo, il programma legge le righe da un file (in questo esempio, "example.txt") utilizzando un'estensione BufferedReader. Ogni riga viene elaborata tramite la funzione pipeline utilizzando il applymetodo, che applica ogni funzione nella pipeline in sequenza e restituisce l'output risultante. L'output risultante viene quindi stampato sulla console utilizzando System.out.println.
Il pacchetto Java util contiene due interfacce funzionali note come "Function<A, B>" e "BiFunction<A, B, C>". L' Functioninterfaccia accetta un singolo input e produce un output, mentre l' BiFunctioninterfaccia accetta due input e produce un output. Ecco un'illustrazione:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
Questo è un chiaro vincolo in quanto ci sono solo due funzioni, con una che accetta un singolo input e l'altra che accetta due input. Di conseguenza, se abbiamo bisogno di una funzione che accetta tre input, dobbiamo costruire la nostra funzione personalizzata.
A tal fine, progettiamo una funzione che accetti tre input e chiamiamola TriFunction. Ecco un esempio di implementazione:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
In situazioni in cui richiediamo più parametri di quelli che le funzioni disponibili possono accettare, possiamo sfruttare il currying per superare questa limitazione.
Possiamo eseguire il refactoring della funzione precedente utilizzando il currying, che implica la scomposizione di una funzione che accetta più argomenti in una sequenza di funzioni, ognuna delle quali accetta un solo argomento. Questo è in linea con la definizione di currying, che è una tecnica impiegata nella Programmazione Funzionale.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
Quanto sopra dichiara un nuovo Functionnome sumWithThreeParamsche accetta un Integerinput. Restituisce un nuovo Functionche prende un Integerinput e restituisce ancora un altro nuovo Functionche prende un Integerinput e restituisce un Integeroutput. sumWithFourParamsAllo stesso modo viene creata una nuova funzione denominata .
Si noti che la funzione con curry sumWithThreeParamse sumWithFourParamsci consente di applicare parzialmente gli argomenti alla funzione, il che può essere utile in situazioni in cui abbiamo alcuni degli input disponibili ma non tutti
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
In questo articolo, abbiamo trattato l'implementazione dei concetti di programmazione funzionale (FP) in Java, come trattare le funzioni come cittadini di prima classe, comporle in pipeline e utilizzare il currying per semplificare funzioni complesse.
Abbiamo fornito esempi utilizzando caratteristiche del linguaggio moderno come java.util.function.Functione java.util.function.BiFunction, così come definito dall'utente TriFunctioncon currying per superare i suoi limiti.
Applicando queste tecniche, gli sviluppatori possono scrivere codice più conciso, riutilizzabile e gestibile in Java.
Con la crescente popolarità di FP nel settore, la comprensione e l'implementazione di questi concetti sta diventando sempre più importante per gli sviluppatori Java.
Dopo aver letto questo articolo, se lo hai trovato utile e vorresti esplorare altri esempi pratici, visita il repository e considera di assegnargli una stella.
Fonte: https://www.freecodecamp.org
#java #functionalprogramming
1678849157
Learn how to implement Functional programming (FP) concepts in Java, including viewing functions as first-class citizens, chaining, and composing them to create function pipelines.
Functional programming (FP) is a programming paradigm. It emphasizes the use of pure functions that don't have side effects and always return the same output for a given input.
This article explores how to implement FP concepts in Java, including viewing functions as first-class citizens, chaining, and composing them to create function pipelines.
We'll also discuss the technique of currying, which allows a function that takes multiple arguments to be transformed into a chain of functions that each take a single argument. This can simplify the use of complex functions and make them more reusable.
In this article, I'll show you examples of how to implement these concepts in Java using modern language features, like “java.util.function.Function”, “java.util.function.BiFunction”, and a user-defined TriFunction. We'll then see how to overcome its limitation using currying.
Table of contents:
java.util.function.Function
interfaceandThen
methodcompose
methodapply()
functionjava.util.function.Function
interfaceThe java.util.function.Function interface is a key component of the Java 8 functional programming API. The java.util.function.Function is a functional interface in Java that takes input of type 'T' and produces an output of type 'R'.
In functional programming, functions are first-class citizens, meaning that they can be passed around as values, stored in variables or data structures, and used as arguments or return values of other functions.
The function interface provides a way to define and manipulate functions in Java code.
The function interface represents a function that takes one input and produces one output. It has a single abstract method, apply()
, which takes an argument of a specified type and returns a result of a specified type.
You can use the function interface to define new functions, as well as to manipulate and compose existing functions. For example, you can use it to convert a list of objects of one type to a list of objects of another type, or to apply a series of transformations to a stream of data.
One of the main benefits of the function interface is that it allows you to write code that is more concise and expressive. By defining functions as values and passing them around as arguments or return values, developers can create more modular and reusable code. Also, by using lambdas to define functions, Java code can be more expressive and easier to read.
You can think about java.util.function.Function
like this:
Function<A, B>
The java.util.function.BiFunction is also a functional interface in Java that takes two inputs 'T' and 'U' and produces an output of type 'R'. In short, BiFunction takes 2 inputs and returns an output:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
You can think about java.util.function.BiFunction
like this:
BiFunction<A, B, C>
Function chaining is a technique in functional programming that involves composing multiple functions into a single pipeline or chain.
In Java FP, function chaining is often used to transform data in a series of steps, where each step applies a specific transformation to the data and passes it on to the next step in the chain.
The function interface in Java provides a powerful tool for function chaining. Each function in the chain is defined as a separate instance of the Function interface. The output of each function becomes the input to the next function in the chain. This allows for a series of transformations to be applied to the data, one after another, until the final result is produced.
andThen
methodThe "andThen()" method is a default method provided by the function interface in Java. This method takes a sequence of two functions and applies them in succession, using the output of the first function as the input to the second function. This chaining of the functions results in a new function that combines the behavior of both functions in a single transformation.
The syntax of andThen
looks like this:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
andThen
We can break the example down into series of steps. Initially the "multiply" function will be executed and its output passed as input to the "add" function. Then the "logOutput" function is used to log the resulting output.
public class FunctionChainingExample {
static Logger logger = Logger.getLogger(FunctionChainingExample.class.getName());
private static Function<Integer, Integer> multiply = x -> x * 2;
private static Function<Integer, Integer> add = x -> x + 2;
private static Function<Integer, Unit> logOutput = x -> {
logger.info("Data:" + x);
return Unit.unit();
};
public static Unit execute(Integer input) {
Function<Integer, Unit> pipeline = multiply
.andThen(add)
.andThen(logOutput);
return pipeline.apply(input);
}
public static void main(String[] args) {
execute(10);
}
}
The the above code snippet showcases the creation of three functions, namely multiply, add, and logOutput.
The function multiply
accepts an integer input and produces an integer output, just like the add
function. But, the logOutput
function accepts an integer input and returns nothing (as represented by the Unit object, which implies the absence of a value).
The execute
function chains the three functions together, where the output of multiply is utilized as the input for the add function, and the resulting output of add is passed to the logOutput function for logging purposes. The above example showcases function chaining using the andThen
default method.
compose
methodUnlike the andThen
method, the compose
method is another default method provided by the function interface in Java. It applies the first function to the output of the second function.
This means that the second function is applied to the input first, and then the first function is applied to the output of the second function. As a result, a chain of functions is created where the output of the second function becomes the input of the first function.
The compose function looks like this:
default Function<V, R> compose(Function<? super V, ? extends T> before)
compose
When to use andThen
vs compose
depends on the order in which you want the functions to be applied.
If you want to apply the functions in the order they are defined, from left to right, then you should use the andThen
method. This method applies the first function to the input, and then applies the second function to the output of the first function.
This is useful when you want to chain together functions that process the input data in a specific order.
On the other hand, if you want to apply the functions in the opposite order, from right to left, then you should use the compose
method. This method applies the second function to the input, and then applies the first function to the output of the second function.
This is useful when you want to chain together functions that need to be applied in the opposite order of the way they are defined.
In general, you should use andThen
when you want to apply the functions in the order they are defined, and use compose
when you want to apply the functions in the opposite order. But the choice between the two methods ultimately depends on the specific requirements of your use case.
public static Unit compose(Integer input) {
Function<Integer, Unit> pipeline = logOutput
.compose(add)
.compose(multiply);
return pipeline.apply(input);
}
In the compose
example, the functions are executed in a right-to-left order. Firstly, multiply
is executed and its output is passed to the add
function. Then, the resulting output of the add
function is passed to logOutput
for logging purposes.
apply()
functionThe apply()
method takes an input and returns a result. It is used to apply a function to an argument and compute a result.
The apply()
function is a method of functional interfaces, such as the function interface, that takes an argument of a specified type and returns a result of a specified type. It is the single abstract method of these interfaces, which is required for them to be used as functional interfaces.
The apply()
function defines the behavior of the functional interface. When an instance of a functional interface is created, the apply()
function is implemented to define what the functional interface does when it is called with an argument.
Function<Integer, Integer> multiply = x -> x * x;
int result = multiply.apply(5);
In this code, we define a function called multiply
that takes an integer argument and returns the square of that integer. We then call the apply()
function on this function, passing in the integer value of 5. The apply()
function executes the lambda expression defined in the square
Function, and returns the result of the computation, which in this case is 25.
Let's understand by looking at a real-world example, FileReaderPipeline.
public class FileReaderPipeline {
static Function<String, String> trim = String::trim;
static Function<String, String> toUpperCase = String::toUpperCase;
static Function<String, String> replaceSpecialCharacters =
str -> str.replaceAll("[^\\p{Alpha}]+", "");
static Function<String, String> pipeline =
trim
.andThen(toUpperCase)
.andThen(replaceSpecialCharacters);
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
String result = pipeline.apply(line);
System.out.println(result);
}
}
}
}
The above code defines a FileReaderPipeline class, which reads lines from a file, processes them through a pipeline of functions, and prints the resulting output.
First, three functions trim
, toUpperCase
, and replaceSpecialCharacters
are defined using method references and lambda expressions. trim
removes leading and trailing whitespace from a String, toUpperCase
converts the string to uppercase, and replaceSpecialCharacters
removes any non-alphabetic characters from the string.
Next, a function pipeline is created using the andThen
method, which chains the three functions together. The input to the pipeline function is a String, and the output is also a String. When a String is passed to the pipeline function, it first removes any leading or trailing whitespace, then converts the string to uppercase, and finally removes any non-alphabetic characters.
Finally, in the main
method, the program reads lines from a file (in this example, "example.txt") using a BufferedReader
. Each line is processed through the pipeline function using the apply
method, which applies each function in the pipeline in sequence, and returns the resulting output. The resulting output is then printed to the console using System.out.println
.
The Java util package contains two functional interfaces known as "Function<A, B>" and "BiFunction<A, B, C>". The Function
interface takes a single input and produces an output, whereas the BiFunction
interface takes two inputs and produces an output. Here is an illustration:
Function<String, String> toUpper = str -> str.toUpperCase();
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
This is a clear constraint as there are only two functions, with one accepting a single input and the other accepting two inputs. Consequently, if we need a function that takes three inputs, we must construct our own customized function.
To this end, let's design a function that accepts three inputs and name it TriFunction
. Here is an example implementation:
@FunctionalInterface
public interface TriFunction<A, B, C, O> {
O apply(A a, B b, C c);
default <R> TriFunction<A, B, C, O> andThen(TriFunction<A, B, C, O> after) {
return (A a, B b, C c) -> after.apply(a,b,c);
}
}
TriFunction<Integer, Integer, Integer, Integer> sum = (a, b, c) -> a + b + c;
int result = sum.apply(1, 2, 3);
In situations where we require more parameters than what the available functions can accept, we can leverage currying to overcome this limitation.
We can refactor the previous function using currying, which involves decomposing a function that takes multiple arguments into a sequence of functions, each of which accepts only one argument. This is in line with the definition of currying, which is a technique employed in Functional Programming.
Function<Integer, Function<Integer, Function<Integer, Integer>>> sumWithThreeParams = (a) -> (b) -> (c) -> a + b + c;
Function<Integer, Function<Integer, Function<Integer, Function<Integer, Integer>>>> sumWithFourParams = (a)->(b)->(c)->(d) -> a + b + c + d;
The above declares a new Function
named sumWithThreeParams
that takes an Integer
input. It returns a new Function
that takes an Integer
input and returns yet another new Function
that takes an Integer
input and returns an Integer
output. Similarly a new Function named sumWithFourParams
is created.
Note that the curried function sumWithThreeParams
and sumWithFourParams
allows us to partially apply arguments to the function, which can be useful in situations where we have some of the inputs available but not all of them
Function<Integer, Function<Integer, Integer>> partialSumWithTwoParams = sumWithThreeParams.apply(5);
Function<Integer, Integer> partialSumWithOneParams
= partialSumWithTwoParams.apply(10);
int c = 15;
int result = partialSumWithOneParams.apply(c);
System.out.println(result); //30
In this article, we covered the implementation of functional programming (FP) concepts in Java, such as treating functions as first-class citizens, composing them into pipelines, and utilizing currying to simplify complex functions.
We have provided examples using modern language features like java.util.function.Function
and java.util.function.BiFunction
, as well as a user-defined TriFunction
with currying to overcome its limitations.
By applying these techniques, developers can write more concise, reusable, and maintainable code in Java.
With the growing popularity of FP in the industry, understanding and implementing these concepts is becoming increasingly important for Java developers.
After reading this article, if you have found it useful and would like to explore more practical examples, please visit the repository and consider giving it a star.
Source: https://www.freecodecamp.org
#java #functionalprogramming
1678564680
Fast, modern, and practical utility library for FP in TypeScript.
ReScript
, which generates highly performant JavaScript code (see the benchmark results here)data-first
approachTypeScript
and Flow
Option
and Result
typesyarn add @mobily/ts-belt
or with npm
npm install @mobily/ts-belt --save
Module | Namespace | Description |
---|---|---|
Array | A | Utility functions for Array . |
Boolean | B | Utility functions for Boolean . |
Number | N | Utility functions for Number . |
Object (Dict) | D | Utility functions for Object . |
String | S | Utility functions for String . |
Guards | G | Various TypeScript guards. |
Option | O | Functions for handling the Option data type that represents the existence and nonexistence of a value. |
Result | R | Functions for describing the result of a certain operation without relying on exceptions. |
Function | F | Other useful functions. |
import { A, O, N, pipe } from '@mobily/ts-belt'
pipe(
[1, 2, 3, 4, 5], // → [1, 2, 3, 4, 5]
A.dropExactly(2), // → Some([3, 4, 5])
O.flatMap(A.head), // → Some(3)
O.map(N.multiply(10)), // → Some(30)
O.getWithDefault(0), // → 30
) // → 30
Full documentation can be found here.
Author: Mobily
Source Code: https://github.com/mobily/ts-belt
License: MIT license
1677739920
(Adapted from monocle site)
Modifying immutable nested object in JavaScript is verbose which makes code difficult to understand and reason about.
Let's have a look at some examples:
interface Street {
num: number
name: string
}
interface Address {
city: string
street: Street
}
interface Company {
name: string
address: Address
}
interface Employee {
name: string
company: Company
}
Let’s say we have an employee and we need to upper case the first character of his company street name. Here is how we could write it in vanilla JavaScript
const employee: Employee = {
name: 'john',
company: {
name: 'awesome inc',
address: {
city: 'london',
street: {
num: 23,
name: 'high street'
}
}
}
}
const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
const employeeCapitalized = {
...employee,
company: {
...employee.company,
address: {
...employee.company.address,
street: {
...employee.company.address.street,
name: capitalize(employee.company.address.street.name)
}
}
}
}
As we can see copy is not convenient to update nested objects because we need to repeat ourselves. Let's see what could we do with monocle-ts
import { Lens } from 'monocle-ts'
const company = Lens.fromProp<Employee>()('company')
const address = Lens.fromProp<Company>()('address')
const street = Lens.fromProp<Address>()('street')
const name = Lens.fromProp<Street>()('name')
compose
takes two Lenses
, one from A
to B
and another one from B
to C
and creates a third Lens
from A
to C
. Therefore, after composing company
, address
, street
and name
, we obtain a Lens
from Employee
to string
(the street name). Now we can use this Lens
issued from the composition to modify the street name using the function capitalize
const capitalizeName = company.compose(address).compose(street).compose(name).modify(capitalize)
assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
You can use the fromPath
API to avoid some boilerplate
import { Lens } from 'monocle-ts'
const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])
const capitalizeName = name.modify(capitalize)
assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized) // true
Here modify
lift a function string => string
to a function Employee => Employee
. It works but it would be clearer if we could zoom into the first character of a string
with a Lens
. However, we cannot write such a Lens
because Lenses
require the field they are directed at to be mandatory. In our case the first character of a string
is optional as a string
can be empty. So we need another abstraction that would be a sort of partial Lens, in monocle-ts
it is called an Optional
.
import { Optional } from 'monocle-ts'
import { some, none } from 'fp-ts/Option'
const firstLetterOptional = new Optional<string, string>(
(s) => (s.length > 0 ? some(s[0]) : none),
(a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
)
const firstLetter = company.compose(address).compose(street).compose(name).asOptional().compose(firstLetterOptional)
assert.deepStrictEqual(firstLetter.modify((s) => s.toUpperCase())(employee), employeeCapitalized)
Similarly to compose
for lenses, compose
for optionals takes two Optionals
, one from A
to B
and another from B
to C
and creates a third Optional
from A
to C
. All Lenses
can be seen as Optionals
where the optional element to zoom into is always present, hence composing an Optional
and a Lens
always produces an Optional
.
TypeScript compatibility
The stable version is tested against TypeScript 3.5.2, but should run with TypeScript 2.8.0+ too
monocle-ts version | required typescript version |
---|---|
2.0.x+ | 3.5+ |
1.x+ | 2.8.0+ |
Note. If you are running < typescript@3.0.1
you have to polyfill unknown
.
You can use unknown-ts as a polyfill.
Documentation
2.3+
)Experimental modules (*) are published in order to get early feedback from the community.
The experimental modules are independent and backward-incompatible with stable ones.
(*) A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice.
From monocle@2.3+
you can use the following experimental modules:
Iso
Lens
Prism
Optional
Traversal
At
Ix
which implement the same features contained in index.ts
but are pipe
-based instead of class
-based.
Here's the same examples with the new API
interface Street {
num: number
name: string
}
interface Address {
city: string
street: Street
}
interface Company {
name: string
address: Address
}
interface Employee {
name: string
company: Company
}
const employee: Employee = {
name: 'john',
company: {
name: 'awesome inc',
address: {
city: 'london',
street: {
num: 23,
name: 'high street'
}
}
}
}
const capitalize = (s: string): string => s.substring(0, 1).toUpperCase() + s.substring(1)
const employeeCapitalized = {
...employee,
company: {
...employee.company,
address: {
...employee.company.address,
street: {
...employee.company.address.street,
name: capitalize(employee.company.address.street.name)
}
}
}
}
import * as assert from 'assert'
import * as L from 'monocle-ts/Lens'
import { pipe } from 'fp-ts/function'
const capitalizeName = pipe(
L.id<Employee>(),
L.prop('company'),
L.prop('address'),
L.prop('street'),
L.prop('name'),
L.modify(capitalize)
)
assert.deepStrictEqual(capitalizeName(employee), employeeCapitalized)
import * as O from 'monocle-ts/Optional'
import { some, none } from 'fp-ts/Option'
const firstLetterOptional: O.Optional<string, string> = {
getOption: (s) => (s.length > 0 ? some(s[0]) : none),
set: (a) => (s) => (s.length > 0 ? a + s.substring(1) : s)
}
const firstLetter = pipe(
L.id<Employee>(),
L.prop('company'),
L.prop('address'),
L.prop('street'),
L.prop('name'),
L.composeOptional(firstLetterOptional)
)
assert.deepStrictEqual(
pipe(
firstLetter,
O.modify((s) => s.toUpperCase())
)(employee),
employeeCapitalized
)
Author: Gcanti
Source Code: https://github.com/gcanti/monocle-ts
License: MIT license
1675834033
Encode failure into your program.
This package contains a Result
type that represents either success (Ok
) or failure (Err
).
For asynchronous tasks, neverthrow
offers a ResultAsync
class which wraps a Promise<Result<T, E>>
and gives you the same level of expressivity and control as a regular Result<T, E>
.
ResultAsync
is thenable
meaning it behaves exactly like a native Promise<Result>
... except you have access to the same methods that Result
provides without having to await
or .then
the promise! Check out the wiki for examples and best practices.
Need to see real-life examples of how to leverage this package for error handling? See this repo: https://github.com/parlez-vous/server
> npm install neverthrow
eslint-plugin-neverthrow
As part of neverthrow
s bounty program, user mdbetancourt created eslint-plugin-neverthrow
to ensure that errors are not gone unhandled.
Install by running:
> npm install eslint-plugin-neverthrow
With eslint-plugin-neverthrow
, you are forced to consume the result in one of the following three ways:
.match
.unwrapOr
._unsafeUnwrap
This ensures that you're explicitly handling the error of your Result
.
This plugin is essentially a porting of Rust's must-use
attribute.
neverthrow
exposes the following:
ok
convenience function to create an Ok
variant of Result
err
convenience function to create an Err
variant of Result
Ok
class and typeErr
class and typeResult
Type as well as namespace / object from which to call Result.fromThrowable
, Result.combine.ResultAsync
classokAsync
convenience function to create a ResultAsync
containing an Ok
type Result
errAsync
convenience function to create a ResultAsync
containing an Err
type Result
import {
ok,
Ok,
err,
Err,
Result,
okAsync,
errAsync,
ResultAsync,
fromThrowable,
fromPromise,
fromSafePromise,
} from 'neverthrow'
Check out the wiki for help on how to make the most of neverthrow
.
If you find this package useful, please consider sponsoring me or simply buying me a coffee!
Result
)ok
Constructs an Ok
variant of Result
Signature:
ok<T, E>(value: T): Ok<T, E> { ... }
Example:
import { ok } from 'neverthrow'
const myResult = ok({ myData: 'test' }) // instance of `Ok`
myResult.isOk() // true
myResult.isErr() // false
err
Constructs an Err
variant of Result
Signature:
err<T, E>(error: E): Err<T, E> { ... }
Example:
import { err } from 'neverthrow'
const myResult = err('Oh noooo') // instance of `Err`
myResult.isOk() // false
myResult.isErr() // true
Result.isOk
(method)Returns true
if the result is an Ok
variant
Signature:
isOk(): boolean { ... }
Result.isErr
(method)Returns true
if the result is an Err
variant
Signature:
isErr(): boolean { ... }
Result.map
(method)Maps a Result<T, E>
to Result<U, E>
by applying a function to a contained Ok
value, leaving an Err
value untouched.
This function can be used to compose the results of two functions.
Signature:
class Result<T, E> {
map<U>(callback: (value: T) => U): Result<U, E> { ... }
}
Example:
const { getLines } from 'imaginary-parser'
// ^ assume getLines has the following signature:
// getLines(str: string): Result<Array<string>, Error>
// since the formatting is deemed correct by `getLines`
// then it means that `linesResult` is an Ok
// containing an Array of strings for each line of code
const linesResult = getLines('1\n2\n3\n4\n')
// this Result now has a Array<number> inside it
const newResult = linesResult.map(
(arr: Array<string>) => arr.map(parseInt)
)
newResult.isOk() // true
Result.mapErr
(method)Maps a Result<T, E>
to Result<T, F>
by applying a function to a contained Err
value, leaving an Ok
value untouched.
This function can be used to pass through a successful result while handling an error.
Signature:
class Result<T, E> {
mapErr<F>(callback: (error: E) => F): Result<T, F> { ... }
}
Example:
import { parseHeaders } 'imaginary-http-parser'
// imagine that parseHeaders has the following signature:
// parseHeaders(raw: string): Result<SomeKeyValueMap, ParseError>
const rawHeaders = 'nonsensical gibberish and badly formatted stuff'
const parseResult = parseHeaders(rawHeaders)
parseResult.mapErr(parseError => {
res.status(400).json({
error: parseError
})
})
parseResult.isErr() // true
Result.unwrapOr
(method)Unwrap the Ok
value, or return the default if there is an Err
Signature:
class Result<T, E> {
unwrapOr<T>(value: T): T { ... }
}
Example:
const myResult = err('Oh noooo')
const multiply = (value: number): number => value * 2
const unwrapped: number = myResult.map(multiply).unwrapOr(10)
Result.andThen
(method)Same idea as map
above. Except you must return a new Result
.
The returned value will be a Result
. As of v4.1.0-beta
, you are able to return distinct error types (see signature below). Prior to v4.1.0-beta
, the error type could not be distinct.
This is useful for when you need to do a subsequent computation using the inner T
value, but that computation might fail.
Additionally, andThen
is really useful as a tool to flatten a Result<Result<A, E2>, E1>
into a Result<A, E2>
(see example below).
Signature:
class Result<T, E> {
// Note that the latest version lets you return distinct errors as well.
// If the error types (E and F) are the same (like `string | string`)
// then they will be merged into one type (`string`)
andThen<U, F>(
callback: (value: T) => Result<U, F>
): Result<U, E | F> { ... }
}
Example 1: Chaining Results
import { err, ok } from 'neverthrow'
const sq = (n: number): Result<number, number> => ok(n ** 2)
ok(2)
.andThen(sq)
.andThen(sq) // Ok(16)
ok(2)
.andThen(sq)
.andThen(err) // Err(4)
ok(2)
.andThen(err)
.andThen(sq) // Err(2)
err(3)
.andThen(sq)
.andThen(sq) // Err(3)
Example 2: Flattening Nested Results
// It's common to have nested Results
const nested = ok(ok(1234))
// notNested is a Ok(1234)
const notNested = nested.andThen((innerResult) => innerResult)
Result.asyncAndThen
(method)Same idea as andThen
above, except you must return a new ResultAsync
.
The returned value will be a ResultAsync
.
Signature:
class Result<T, E> {
asyncAndThen<U, F>(
callback: (value: T) => ResultAsync<U, F>
): ResultAsync<U, E | F> { ... }
}
Result.orElse
(method)Takes an Err
value and maps it to a Result<T, SomeNewType>
. This is useful for error recovery.
Signature:
class Result<T, E> {
orElse<A>(
callback: (error: E) => Result<T, A>
): Result<T, A> { ... }
}
Example:
enum DatabaseError {
PoolExhausted = 'PoolExhausted',
NotFound = 'NotFound',
}
const dbQueryResult: Result<string, DatabaseError> = err(DatabaseError.NotFound)
const updatedQueryResult = dbQueryResult.orElse((dbError) =>
dbError === DatabaseError.NotFound
? ok('User does not exist') // error recovery branch: ok() must be called with a value of type string
//
//
// err() can be called with a value of any new type that you want
// it could also be called with the same error value
//
// err(dbError)
: err(500)
)
Result.match
(method)Given 2 functions (one for the Ok
variant and one for the Err
variant) execute the function that matches the Result
variant.
Match callbacks do not necessitate to return a Result
, however you can return a Result
if you want to.
Signature:
class Result<T, E> {
match<A>(
okCallback: (value: T) => A,
errorCallback: (error: E) => A
): A => { ... }
}
match
is like chaining map
and mapErr
, with the distinction that with match
both functions must have the same return type. The differences between match
and chaining map
and mapErr
are that:
match
both functions must have the same return type A
match
unwraps the Result<T, E>
into an A
(the match functions' return type)Example:
// map/mapErr api
// note that you DON'T have to append mapErr
// after map which means that you are not required to do
// error handling
computationThatMightFail().map(console.log).mapErr(console.error)
// match api
// works exactly the same as above since both callbacks
// only perform side effects,
// except, now you HAVE to do error handling :)
computationThatMightFail().match(console.log, console.error)
// Returning values
const attempt = computationThatMightFail()
.map((str) => str.toUpperCase())
.mapErr((err) => `Error: ${err}`)
// `attempt` is of type `Result<string, string>`
const answer = computationThatMightFail().match(
(str) => str.toUpperCase(),
(err) => `Error: ${err}`
)
// `answer` is of type `string`
If you don't use the error parameter in your match callback then match
is equivalent to chaining map
with unwrapOr
:
const answer = computationThatMightFail().match(
(str) => str.toUpperCase(),
() => 'ComputationError'
)
// `answer` is of type `string`
const answer = computationThatMightFail()
.map((str) => str.toUpperCase())
.unwrapOr('ComputationError')
Result.asyncMap
(method)Similar to map
except for two things:
Promise
ResultAsync
You can then chain the result of asyncMap
using the ResultAsync
apis (like map
, mapErr
, andThen
, etc.)
Signature:
class Result<T, E> {
asyncMap<U>(
callback: (value: T) => Promise<U>
): ResultAsync<U, E> { ... }
}
Example:
import { parseHeaders } 'imaginary-http-parser'
// imagine that parseHeaders has the following signature:
// parseHeaders(raw: string): Result<SomeKeyValueMap, ParseError>
const asyncRes = parseHeaders(rawHeader)
.map(headerKvMap => headerKvMap.Authorization)
.asyncMap(findUserInDatabase)
Note that in the above example if parseHeaders
returns an Err
then .map
and .asyncMap
will not be invoked, and asyncRes
variable will resolve to an Err
when turned into a Result
using await
or .then()
.
Result.fromThrowable
(static class method)Although Result is not an actual JS class, the way that
fromThrowable
has been implemented requires that you callfromThrowable
as though it were a static method onResult
. See examples below.
The JavaScript community has agreed on the convention of throwing exceptions. As such, when interfacing with third party libraries it's imperative that you wrap third-party code in try / catch blocks.
This function will create a new function that returns an Err
when the original function throws.
It is not possible to know the types of the errors thrown in the original function, therefore it is recommended to use the second argument errorFn
to map what is thrown to a known type.
Example:
import { Result } from 'neverthrow'
type ParseError = { message: string }
const toParseError = (): ParseError => ({ message: "Parse Error" })
const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError)
// the function can now be used safely, if the function throws, the result will be an Err
const res = safeJsonParse("{");
Result.combine
(static class method)Although Result is not an actual JS class, the way that
combine
has been implemented requires that you callcombine
as though it were a static method onResult
. See examples below.
Combine lists of Result
s.
If you're familiar with Promise.all
, the combine function works conceptually the same.
combine
works on both heterogeneous and homogeneous lists. This means that you can have lists that contain different kinds of Result
s and still be able to combine them. Note that you cannot combine lists that contain both Result
s and ResultAsync
s.
The combine function takes a list of results and returns a single result. If all the results in the list are Ok
, then the return value will be a Ok
containing a list of all the individual Ok
values.
If just one of the results in the list is an Err
then the combine function returns that Err value (it short circuits and returns the first Err that it finds).
Formally speaking:
// homogeneous lists
function combine<T, E>(resultList: Result<T, E>[]): Result<T[], E>
// heterogeneous lists
function combine<T1, T2, E1, E2>(resultList: [ Result<T1, E1>, Result<T2, E2> ]): Result<[ T1, T2 ], E1 | E2>
function combine<T1, T2, T3, E1, E2, E3> => Result<[ T1, T2, T3 ], E1 | E2 | E3>
function combine<T1, T2, T3, T4, E1, E2, E3, E4> => Result<[ T1, T2, T3, T4 ], E1 | E2 | E3 | E4>
// ... etc etc ad infinitum
Example:
const resultList: Result<number, never>[] =
[ok(1), ok(2)]
const combinedList: Result<number[], unknown> =
Result.combine(resultList)
Example with tuples:
/** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */
const tuple = <T extends any[]>(...args: T): T => args
const resultTuple: [Result<string, never>, Result<string, never>] =
tuple(ok('a'), ok('b'))
const combinedTuple: Result<[string, string], unknown> =
Result.combine(resultTuple)
Result.combineWithAllErrors
(static class method)Although Result is not an actual JS class, the way that
combineWithAllErrors
has been implemented requires that you callcombineWithAllErrors
as though it were a static method onResult
. See examples below.
Like combine
but without short-circuiting. Instead of just the first error value, you get a list of all error values of the input result list.
If only some results fail, the new combined error list will only contain the error value of the failed results, meaning that there is no guarantee of the length of the new error list.
Function signature:
// homogeneous lists
function combineWithAllErrors<T, E>(resultList: Result<T, E>[]): Result<T[], E[]>
// heterogeneous lists
function combineWithAllErrors<T1, T2, E1, E2>(resultList: [ Result<T1, E1>, Result<T2, E2> ]): Result<[ T1, T2 ], (E1 | E2)[]>
function combineWithAllErrors<T1, T2, T3, E1, E2, E3> => Result<[ T1, T2, T3 ], (E1 | E2 | E3)[]>
function combineWithAllErrors<T1, T2, T3, T4, E1, E2, E3, E4> => Result<[ T1, T2, T3, T4 ], (E1 | E2 | E3 | E4)[]>
// ... etc etc ad infinitum
Example usage:
const resultList: Result<number, string>[] = [
ok(123),
err('boooom!'),
ok(456),
err('ahhhhh!'),
]
const result = Result.combineWithAllErrors(resultList)
// result is Err(['boooom!', 'ahhhhh!'])
ResultAsync
)okAsync
Constructs an Ok
variant of ResultAsync
Signature:
okAsync<T, E>(value: T): ResultAsync<T, E>
Example:
import { okAsync } from 'neverthrow'
const myResultAsync = okAsync({ myData: 'test' }) // instance of `ResultAsync`
const myResult = await myResultAsync // instance of `Ok`
myResult.isOk() // true
myResult.isErr() // false
errAsync
Constructs an Err
variant of ResultAsync
Signature:
errAsync<T, E>(error: E): ResultAsync<T, E>
Example:
import { errAsync } from 'neverthrow'
const myResultAsync = errAsync('Oh nooo') // instance of `ResultAsync`
const myResult = await myResultAsync // instance of `Err`
myResult.isOk() // false
myResult.isErr() // true
ResultAsync.fromPromise
(static class method)Transforms a PromiseLike<T>
(that may throw) into a ResultAsync<T, E>
.
The second argument handles the rejection case of the promise and maps the error from unknown
into some type E
.
Signature:
// fromPromise is a static class method
// also available as a standalone function
// import { fromPromise } from 'neverthrow'
ResultAsync.fromPromise<T, E>(
promise: PromiseLike<T>,
errorHandler: (unknownError: unknown) => E)
): ResultAsync<T, E> { ... }
If you are working with PromiseLike
objects that you know for a fact will not throw, then use fromSafePromise
in order to avoid having to pass a redundant errorHandler
argument.
Example:
import { ResultAsync } from 'neverthrow'
import { insertIntoDb } from 'imaginary-database'
// insertIntoDb(user: User): Promise<User>
const res = ResultAsync.fromPromise(insertIntoDb(myUser), () => new Error('Database error'))
// `res` has a type of ResultAsync<User, Error>
ResultAsync.fromSafePromise
(static class method)Same as ResultAsync.fromPromise
except that it does not handle the rejection of the promise. Ensure you know what you're doing, otherwise a thrown exception within this promise will cause ResultAsync to reject, instead of resolve to a Result.
Signature:
// fromPromise is a static class method
// also available as a standalone function
// import { fromPromise } from 'neverthrow'
ResultAsync.fromSafePromise<T, E>(
promise: PromiseLike<T>
): ResultAsync<T, E> { ... }
Example:
import { RouteError } from 'routes/error'
// simulate slow routes in an http server that works in a Result / ResultAsync context
// Adopted from https://github.com/parlez-vous/server/blob/2496bacf55a2acbebc30631b5562f34272794d76/src/routes/common/signup.ts
export const slowDown = <T>(ms: number) => (value: T) =>
ResultAsync.fromSafePromise<T, RouteError>(
new Promise((resolve) => {
setTimeout(() => {
resolve(value)
}, ms)
})
)
export const signupHandler = route<User>((req, sessionManager) =>
decode(userSignupDecoder, req.body, 'Invalid request body').map((parsed) => {
return createUser(parsed)
.andThen(slowDown(3000)) // slowdown by 3 seconds
.andThen(sessionManager.createSession)
.map(({ sessionToken, admin }) => AppData.init(admin, sessionToken))
})
)
ResultAsync.map
(method)Maps a ResultAsync<T, E>
to ResultAsync<U, E>
by applying a function to a contained Ok
value, leaving an Err
value untouched.
The applied function can be synchronous or asynchronous (returning a Promise<U>
) with no impact to the return type.
This function can be used to compose the results of two functions.
Signature:
class ResultAsync<T, E> {
map<U>(
callback: (value: T) => U | Promise<U>
): ResultAsync<U, E> { ... }
}
Example:
const { findUsersIn } from 'imaginary-database'
// ^ assume findUsersIn has the following signature:
// findUsersIn(country: string): ResultAsync<Array<User>, Error>
const usersInCanada = findUsersIn("Canada")
// Let's assume we only need their names
const namesInCanada = usersInCanada.map((users: Array<User>) => users.map(user => user.name))
// namesInCanada is of type ResultAsync<Array<string>, Error>
// We can extract the Result using .then() or await
namesInCanada.then((namesResult: Result<Array<string>, Error>) => {
if(namesResult.isErr()){
console.log("Couldn't get the users from the database", namesResult.error)
}
else{
console.log("Users in Canada are named: " + namesResult.value.join(','))
}
})
ResultAsync.mapErr
(method)Maps a ResultAsync<T, E>
to ResultAsync<T, F>
by applying a function to a contained Err
value, leaving an Ok
value untouched.
The applied function can be synchronous or asynchronous (returning a Promise<F>
) with no impact to the return type.
This function can be used to pass through a successful result while handling an error.
Signature:
class ResultAsync<T, E> {
mapErr<F>(
callback: (error: E) => F | Promise<F>
): ResultAsync<T, F> { ... }
}
Example:
const { findUsersIn } from 'imaginary-database'
// ^ assume findUsersIn has the following signature:
// findUsersIn(country: string): ResultAsync<Array<User>, Error>
// Let's say we need to low-level errors from findUsersIn to be more readable
const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => {
// The only error we want to pass to the user is "Unknown country"
if(error.message === "Unknown country"){
return error.message
}
// All other errors will be labelled as a system error
return "System error, please contact an administrator."
})
// usersInCanada is of type ResultAsync<Array<User>, string>
usersInCanada.then((usersResult: Result<Array<User>, string>) => {
if(usersResult.isErr()){
res.status(400).json({
error: usersResult.error
})
}
else{
res.status(200).json({
users: usersResult.value
})
}
})
ResultAsync.unwrapOr
(method)Unwrap the Ok
value, or return the default if there is an Err
.
Works just like Result.unwrapOr
but returns a Promise<T>
instead of T
.
Signature:
class ResultAsync<T, E> {
unwrapOr<T>(value: T): Promise<T> { ... }
}
Example:
const unwrapped: number = await errAsync(0).unwrapOr(10)
// unwrapped = 10
ResultAsync.andThen
(method)Same idea as map
above. Except the applied function must return a Result
or ResultAsync
.
ResultAsync.andThen
always returns a ResultAsync
no matter the return type of the applied function.
This is useful for when you need to do a subsequent computation using the inner T
value, but that computation might fail.
andThen
is really useful as a tool to flatten a ResultAsync<ResultAsync<A, E2>, E1>
into a ResultAsync<A, E2>
(see example below).
Signature:
// Note that the latest version (v4.1.0-beta) lets you return distinct errors as well.
// If the error types (E and F) are the same (like `string | string`)
// then they will be merged into one type (`string`)
class ResultAsync<T, E> {
andThen<U, F>(
callback: (value: T) => Result<U, F> | ResultAsync<U, F>
): ResultAsync<U, E | F> { ... }
}
Example
const { validateUser } from 'imaginary-validator'
const { insertUser } from 'imaginary-database'
const { sendNotification } from 'imaginary-service'
// ^ assume validateUser, insertUser and sendNotification have the following signatures:
// validateUser(user: User): Result<User, Error>
// insertUser(user): ResultAsync<User, Error>
// sendNotification(user): ResultAsync<void, Error>
const resAsync = validateUser(user)
.andThen(insertUser)
.andThen(sendNotification)
// resAsync is a ResultAsync<void, Error>
resAsync.then((res: Result<void, Error>) => {
if(res.isErr()){
console.log("Oops, at least one step failed", res.error)
}
else{
console.log("User has been validated, inserted and notified successfully.")
}
})
ResultAsync.orElse
(method)Takes an Err
value and maps it to a ResultAsync<T, SomeNewType>
. This is useful for error recovery.
Signature:
class ResultAsync<T, E> {
orElse<A>(
callback: (error: E) => Result<T, A> | ResultAsync<T, A>
): ResultAsync<T, A> { ... }
}
ResultAsync.match
(method)Given 2 functions (one for the Ok
variant and one for the Err
variant) execute the function that matches the ResultAsync
variant.
The difference with Result.match
is that it always returns a Promise
because of the asynchronous nature of the ResultAsync
.
Signature:
class ResultAsync<T, E> {
match<A>(
okCallback: (value: T) => A,
errorCallback: (error: E) => A
): Promise<A> => { ... }
}
Example:
const { validateUser } from 'imaginary-validator'
const { insertUser } from 'imaginary-database'
// ^ assume validateUser and insertUser have the following signatures:
// validateUser(user: User): Result<User, Error>
// insertUser(user): ResultAsync<User, Error>
// Handle both cases at the end of the chain using match
const resultMessage = await validateUser(user)
.andThen(insertUser)
.match(
(user: User) => `User ${user.name} has been successfully created`,
(error: Error) => `User could not be created because ${error.message}`
)
// resultMessage is a string
ResultAsync.combine
(static class method)Combine lists of ResultAsync
s.
If you're familiar with Promise.all
, the combine function works conceptually the same.
combine
works on both heterogeneous and homogeneous lists. This means that you can have lists that contain different kinds of ResultAsync
s and still be able to combine them. Note that you cannot combine lists that contain both Result
s and ResultAsync
s.
The combine function takes a list of results and returns a single result. If all the results in the list are Ok
, then the return value will be a Ok
containing a list of all the individual Ok
values.
If just one of the results in the list is an Err
then the combine function returns that Err value (it short circuits and returns the first Err that it finds).
Formally speaking:
// homogeneous lists
function combine<T, E>(resultList: ResultAsync<T, E>[]): ResultAsync<T[], E>
// heterogeneous lists
function combine<T1, T2, E1, E2>(resultList: [ ResultAsync<T1, E1>, ResultAsync<T2, E2> ]): ResultAsync<[ T1, T2 ], E1 | E2>
function combine<T1, T2, T3, E1, E2, E3> => ResultAsync<[ T1, T2, T3 ], E1 | E2 | E3>
function combine<T1, T2, T3, T4, E1, E2, E3, E4> => ResultAsync<[ T1, T2, T3, T4 ], E1 | E2 | E3 | E4>
// ... etc etc ad infinitum
Example:
const resultList: ResultAsync<number, never>[] =
[okAsync(1), okAsync(2)]
const combinedList: ResultAsync<number[], unknown> =
ResultAsync.combine(resultList)
Example with tuples:
/** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */
const tuple = <T extends any[]>(...args: T): T => args
const resultTuple: [ResultAsync<string, never>, ResultAsync<string, never>] =
tuple(okAsync('a'), okAsync('b'))
const combinedTuple: ResultAsync<[string, string], unknown> =
ResultAsync.combine(resultTuple)
ResultAsync.combineWithAllErrors
(static class method)Like combine
but without short-circuiting. Instead of just the first error value, you get a list of all error values of the input result list.
If only some results fail, the new combined error list will only contain the error value of the failed results, meaning that there is no guarantee of the length of the new error list.
Function signature:
// homogeneous lists
function combineWithAllErrors<T, E>(resultList: ResultAsync<T, E>[]): ResultAsync<T[], E[]>
// heterogeneous lists
function combineWithAllErrors<T1, T2, E1, E2>(resultList: [ ResultAsync<T1, E1>, ResultAsync<T2, E2> ]): ResultAsync<[ T1, T2 ], (E1 | E2)[]>
function combineWithAllErrors<T1, T2, T3, E1, E2, E3> => ResultAsync<[ T1, T2, T3 ], (E1 | E2 | E3)[]>
function combineWithAllErrors<T1, T2, T3, T4, E1, E2, E3, E4> => ResultAsync<[ T1, T2, T3, T4 ], (E1 | E2 | E3 | E4)[]>
// ... etc etc ad infinitum
Example usage:
const resultList: ResultAsync<number, string>[] = [
okAsync(123),
errAsync('boooom!'),
okAsync(456),
errAsync('ahhhhh!'),
]
const result = ResultAsync.combineWithAllErrors(resultList)
// result is Err(['boooom!', 'ahhhhh!'])
fromThrowable
Top level export of Result.fromThrowable
. Please find documentation at Result.fromThrowable
fromPromise
Top level export of ResultAsync.fromPromise
. Please find documentation at ResultAsync.fromPromise
fromSafePromise
Top level export of ResultAsync.fromSafePromise
. Please find documentation at ResultAsync.fromSafePromise
Result
instances have two unsafe methods, aptly called _unsafeUnwrap
and _unsafeUnwrapErr
which should only be used in a test environment.
_unsafeUnwrap
takes a Result<T, E>
and returns a T
when the result is an Ok
, otherwise it throws a custom object.
_unsafeUnwrapErr
takes a Result<T, E>
and returns a E
when the result is an Err
, otherwise it throws a custom object.
That way you can do something like:
expect(myResult._unsafeUnwrap()).toBe(someExpectation)
However, do note that Result
instances are comparable. So you don't necessarily need to unwrap them in order to assert expectations in your tests. So you could also do something like this:
import { ok } from 'neverthrow'
// ...
expect(callSomeFunctionThatReturnsAResult("with", "some", "args")).toEqual(ok(someExpectation));
By default, the thrown value does not contain a stack trace. This is because stack trace generation makes error messages in Jest harder to understand. If you want stack traces to be generated, call _unsafeUnwrap
and / or _unsafeUnwrapErr
with a config object:
_unsafeUnwrapErr({
withStackTrace: true,
})
// ^ Now the error object will have a `.stack` property containing the current stack
If you find this package useful, please consider sponsoring me or simply buying me a coffee!
Although the package is called neverthrow
, please don't take this literally. I am simply encouraging the developer to think a bit more about the ergonomics and usage of whatever software they are writing.
Throw
ing and catching
is very similar to using goto
statements - in other words; it makes reasoning about your programs harder. Secondly, by using throw
you make the assumption that the caller of your function is implementing catch
. This is a known source of errors. Example: One dev throw
s and another dev uses the function without prior knowledge that the function will throw. Thus, and edge case has been left unhandled and now you have unhappy users, bosses, cats, etc.
With all that said, there are definitely good use cases for throwing in your program. But much less than you might think.
Author: Supermacro
Source Code: https://github.com/supermacro/neverthrow
License: MIT license
1674896711
This is a simplified implementation of TypeScript's type system that's written in TypeScript's type annotations. This means that it uses types only — with no runtime code whatsoever.
You pass TypeScript code as a string to the TypeCheck
generic and get possible type errors back (See the live demo):
☝ Please note that this project is meant to be used for fun and learning purposes and not for practical use.
See a live demo in your browser on the TypeScript Playground.
Alternatively, install @ronami/hypescript
in your project with yarn
or npm
(TypeScript 4.7 or later is required):
yarn add @ronami/hypescript
Only a subset of TypeScript's syntax and features are available. Here's a list of examples (with browser demo links) for some code examples:
Author: Ronami
Source Code: https://github.com/ronami/HypeScript
License: MIT license