Meo! Bắt đầu sử dụng Mèo trong dự án của bạn ngay bây giờ

Giới thiệu nhẹ nhàng về thư viện Mèo.

Giới thiệu

Mèo là một thư viện cung cấp trừu tượng cho lập trình chức năng trong Scala.

Có một số bài viết và khóa học tuyệt vời về Mèo trên mạng (như mèo Herding và hướng dẫn về Bài tập Scala), nhưng họ có xu hướng khám phá các danh mục / loại lớp được triển khai trong thư viện thay vì sẵn sàng thực tế sử dụng các ví dụ về cách sử dụng Mèo trong các cơ sở mã hiện có. Bài đăng trên blog này hầu như không làm trầy xước bề mặt của những gì Mèo có thể làm, nhưng thay vào đó cung cấp một giới thiệu thực hành ngắn gọn về các mẫu mà bạn có thể tận dụng trong dự án Scala của bạn. Nếu bạn sử dụng bất kỳ đơn vị nào như Tương lai hoặc Tùy chọn hàng ngày, thì rất có thể Mèo có thể đơn giản hóa và cải thiện khả năng đọc mã của bạn.

Vui lòng tham khảo wiki Mèo trên GitHub để biết hướng dẫn về cách thêm thư viện vào phụ thuộc dự án của bạn. Chúng tôi đang theo sát phiên bản 0.9.0 trong toàn bộ bài viết.

Hãy để cuốn sách thông qua gói thư viện, xem xét cú pháp có sẵn trong mỗi gói.

Người trợ giúp cho Tùy chọn và Hoặc

nhập khẩu mèo.syntax.option._

Nhập gói này cho phép cú pháp obj.some - tương đương với Một số (obj). Sự khác biệt thực sự duy nhất là giá trị đã được nâng lên thành Tùy chọn [T] từ Một số [T].

Sử dụng obj.some thay vì Một số (obj) đôi khi có thể cải thiện khả năng đọc của các bài kiểm tra đơn vị. Ví dụ: nếu bạn thêm lớp ẩn sau vào BaseSpec, TestHelper hoặc bất cứ thứ gì lớp cơ sở của bạn để kiểm tra được gọi là:

sau đó bạn có thể sử dụng cú pháp chuỗi được hiển thị bên dưới (giả sử các bài kiểm tra đơn vị của bạn dựa trên scalamock; cũng xem bài đăng của Bartosz Kowalik):

Điều đó có thể đọc được nhiều hơn so với Future.successful (Một số (người dùng)), đặc biệt nếu mẫu này lặp lại thường xuyên trong bộ thử nghiệm. Xâu chuỗi .some.asFuture ở cuối thay vì đặt nó ở phía trước cũng giúp tập trung vào những gì mà thực sự được trả lại thay vì vào loại trình bao bọc dự kiến.

lần lượt, không có [T] nào là tốc ký cho Option.empty [T] chỉ là Không có, nhưng đã bị phát tán từ Tùy chọn none.typeto [T]. Việc cung cấp một loại chuyên biệt hơn đôi khi giúp trình biên dịch Scala suy ra đúng loại biểu thức có chứa Không có.

nhập khẩu mèo.syntax.either._

obj.asRight là phải (obj), obj.asLeft là trái (obj). Trong cả hai trường hợp, loại giá trị trả về được mở rộng từ Phải hoặc Trái sang Hoặc. Cũng giống như trường hợp của .some, những trợ giúp này rất tiện để kết hợp với .asFuture để cải thiện khả năng đọc của các bài kiểm tra đơn vị:

Either.fromOption (tùy chọn: Tùy chọn [A], ifNone: => E), lần lượt, là một trợ giúp hữu ích để chuyển đổi Tùy chọn thành Either. Nếu tùy chọn được cung cấp là Một số (x), thì nó trở thành Phải (x). Nếu không, nó trở thành Trái với giá trị ifNone được cung cấp bên trong.

gói trường hợp và cú pháp cartesian

nhập khẩu mèo.instances. ._

Có một vài lớp loại cơ bản cho Mèo (và nói chung là lập trình chức năng dựa trên danh mục), những lớp quan trọng nhất là Functor, Applicative và Monad. Chúng tôi sẽ không đi sâu vào chi tiết trong bài đăng trên blog này (xem ví dụ như hướng dẫn đã được đề cập), nhưng điều quan trọng cần biết là để sử dụng hầu hết cú pháp Mèo, bạn cũng cần nhập các thể hiện lớp loại ẩn cho các cấu trúc bạn ' Đang hoạt động với.

Thông thường, nó chỉ đủ để nhập gói cat.instances thích hợp. Ví dụ: khi bạn đang thực hiện các chuyển đổi trên tương lai, bạn sẽ cần phải nhập cat.instances.future._. Các gói tương ứng cho các tùy chọn và danh sách được gọi là cat.instances.option._ và cat.instances.list._. Chúng cung cấp các thể hiện lớp kiểu ngầm định mà cú pháp Mèo cần để hoạt động đúng.

Là một lưu ý phụ, nếu bạn gặp khó khăn trong việc tìm kiếm các phiên bản hoặc gói cú pháp cần thiết, cách giải quyết nhanh là chỉ cần nhập cat.implicits._. Tuy nhiên, đây không phải là một giải pháp ưa thích vì nó có thể tăng đáng kể thời gian biên dịch - đặc biệt nếu được sử dụng trong nhiều tệp trên toàn dự án. Nó thường được coi là thực hành tốt để sử dụng nhập khẩu hẹp để giảm một số gánh nặng giải quyết ngầm ra khỏi trình biên dịch.

nhập khẩu mèo.syntax.cartesian._

Gói cartesian cung cấp | @ | cú pháp, cho phép xây dựng trực quan để áp dụng một hàm có nhiều hơn một tham số cho nhiều giá trị hiệu quả (như tương lai).

Hãy nói rằng chúng ta có 3 tương lai, một loại Int, một loại String, một loại User và một phương thức chấp nhận ba tham số - Int, String và User.

Mục tiêu của chúng tôi là áp dụng hàm cho các giá trị được tính theo 3 tương lai đó. Với cú pháp cartesian, điều này trở nên rất dễ dàng và súc tích:

Như đã chỉ ra trước đây, để cung cấp ví dụ ẩn (cụ thể là Cartesian [Tương lai]) cần thiết cho | @ | Để hoạt động đúng, bạn nên nhập cat.instances.future._.

Ý tưởng trên có thể được thể hiện thậm chí ngắn hơn, chỉ:

Kết quả của biểu thức trên sẽ thuộc loại Tương lai [TreatmentResult]. Nếu bất kỳ tương lai bị xiềng xích nào thất bại, tương lai kết quả cũng sẽ thất bại với ngoại lệ tương tự như tương lai thất bại đầu tiên trong chuỗi (đây là hành vi thất bại nhanh). Điều quan trọng là, tất cả các hợp đồng tương lai sẽ chạy song song, trái ngược với những gì sẽ xảy ra trong một sự hiểu biết:

Trong đoạn mã trên (mà dưới mui xe dịch sang các bản đồ phẳng và các cuộc gọi bản đồ), chuỗiFuture sẽ không chạy cho đến khi intFuture được hoàn thành thành công và theo cách tương tự userFuture sẽ chỉ được chạy sau khi chuỗiFuture hoàn thành. Nhưng vì các tính toán độc lập với nhau, nên nó hoàn toàn khả thi để chạy chúng song song với | @ | thay thế.

Đi qua

nhập mèo.syntax.traverse._

đi qua

Nếu bạn có một obj loại F [A] có thể được ánh xạ (như Tương lai) và một chức năng thú vị của loại A => G [B], thì việc gọi obj.map (vui vẻ) sẽ cung cấp cho bạn F [G [ B]]. Trong nhiều trường hợp thực tế phổ biến, như khi F là Tùy chọn và G là Tương lai, bạn sẽ nhận được Tùy chọn [Tương lai [B]], rất có thể là những gì bạn muốn.

đi qua như là một giải pháp ở đây. Nếu bạn gọi di chuyển thay vì bản đồ, như obj.traverse (vui vẻ), bạn sẽ nhận được G [F [A]], đó sẽ là Tương lai [Tùy chọn [B]] trong trường hợp của chúng tôi; điều này hữu ích và dễ xử lý hơn nhiều so với Tùy chọn [Tương lai [B]].

Ngoài ra, còn có một phương thức dành riêng cho Future.traverse trong đối tượng đồng hành Tương lai, nhưng phiên bản Mèo dễ đọc hơn nhiều và có thể dễ dàng làm việc trên bất kỳ cấu trúc nào có sẵn các loại lớp nhất định.

trình tự

trình tự đại diện cho một khái niệm thậm chí đơn giản hơn: có thể được coi là chỉ cần hoán đổi các loại từ F [G [A]] sang G [F [A]] mà không cần ánh xạ giá trị kèm theo như traverse.

obj. resultence trên thực tế được triển khai ở Mèo dưới dạng obj.traverse (danh tính). Mặt khác, obj.traverse (vui vẻ) gần tương đương với obj.map (vui vẻ). Hậu quả.

căn hộ

Nếu bạn có obj loại F [A] và chức năng thú vị của loại A => G [F [B]], thì thực hiện obj.map (f) mang lại kết quả của loại F [G [F [B]]] - rất khó có thể là những gì bạn muốn.

Đi qua obj thay vì ánh xạ sẽ giúp một chút - thay vào đó bạn sẽ nhận được G [F [F [B]]. Vì G thường là một cái gì đó giống như Tương lai và F là Danh sách hoặc Tùy chọn, bạn sẽ kết thúc với Tương lai [Tùy chọn [Tùy chọn [A]] hoặc Tương lai [Danh sách [Danh sách [A]]] - hơi khó xử lý.

Giải pháp có thể là ánh xạ kết quả với một cuộc gọi _.flatten như:

và bằng cách này, bạn sẽ nhận được loại G [F [B]] mong muốn ở cuối.

Tuy nhiên, có một lối tắt gọn gàng cho cái này được gọi là FlatTraverse:

và điều đó giải quyết vấn đề của chúng tôi cho tốt.

Máy biến áp đơn nguyên

nhập khẩu mèo.data.OptionT

Một thể hiện của OptionT [F, A] có thể được coi là một trình bao bọc trên F [Tùy chọn [A]] có thêm một vài phương thức hữu ích dành riêng cho các loại lồng nhau có sẵn trong F hoặc Tùy chọn. Thông thường nhất, F của bạn sẽ là Tương lai (hoặc đôi khi lắt léo DBIO, nhưng điều này đòi hỏi phải thực hiện các lớp loại Mèo như Functor hoặc Monad cho DBIO). Các Wrappers như OptionT thường được gọi là máy biến áp đơn nguyên.

Một mẫu khá phổ biến là ánh xạ giá trị bên trong được lưu trữ bên trong một thể hiện của F [Tùy chọn [A]] thành một thể hiện của F [Tùy chọn [B]] với chức năng loại A => B. Điều này có thể được thực hiện với cú pháp khá dài dòng như:

Với việc sử dụng OptionT, điều này có thể được đơn giản hóa như sau:

Bản đồ trên sẽ trả về giá trị của loại OptionT [Tương lai, Chuỗi].

Để nhận giá trị Tương lai [Tùy chọn [Chuỗi]] bên dưới, chỉ cần gọi .value trên đối tượng OptionT. Nó cũng là một giải pháp khả thi để chuyển hoàn toàn sang OptionT [Tương lai, A] trong các loại tham số / trả về phương thức và hoàn toàn (hoặc gần như hoàn toàn) mương Tương lai [Tùy chọn [A]] trong khai báo kiểu.

Có một số cách để xây dựng một cá thể OptionT. Các tiêu đề phương thức trong bảng dưới đây được đơn giản hóa một chút: các tham số kiểu và các lớp loại được yêu cầu bởi mỗi phương thức được bỏ qua.

Trong mã sản xuất, bạn thường sử dụng cú pháp OptionT (...) để bọc một thể hiện của Tương lai [Tùy chọn [A]] vào Tùy chọn [F, A]. Các phương thức khác, lần lượt, chứng minh hữu ích để thiết lập các giá trị giả được nhập bằng OptionT trong các thử nghiệm đơn vị.

Chúng tôi đã bắt gặp một trong những phương pháp của OptionT, cụ thể là bản đồ. Có một số phương thức khác có sẵn và chúng chủ yếu khác nhau bởi chữ ký của hàm mà chúng chấp nhận làm tham số. Như trường hợp của bảng trước, các lớp loại dự kiến ​​sẽ bị bỏ qua.

Trong thực tế, bạn rất có thể sử dụng bản đồ và semiflatMap.

Như mọi khi với FlatMap và bản đồ, bạn có thể sử dụng nó không chỉ rõ ràng, mà còn dưới mui xe trong các nhận thức, như trong ví dụ dưới đây:

Ví dụ OptionT [Tương lai, Tiền] được trả về bởi getReservedFundsForUser sẽ kèm theo một Nonevalue nếu bất kỳ phương thức nào trong ba phương thức tổng hợp trả về một OptionT tương ứng với Không có. Mặt khác, nếu kết quả của cả ba cuộc gọi có chứa Một số, kết quả cuối cùng cũng sẽ chứa Một số cuộc gọi.

nhập khẩu mèo.data.EitherT

EitherT [F, A, B] là biến áp đơn cho Either - bạn có thể nghĩ về nó như một trình bao bọc trên giá trị F [Hoặc [A, B]].

Cũng giống như trong phần trên, tôi đã đơn giản hóa các tiêu đề phương thức, bỏ qua các tham số loại hoặc giới hạn ngữ cảnh và giới hạn dưới của chúng.

Hãy để có một cái nhìn nhanh về cách tạo một thể hiện EitherT:

Chỉ cần làm rõ: EitherT.fromEither kết hợp Either được cung cấp thành F, trong khi EitherT.right và EitherT.left lần lượt bọc giá trị bên trong F được cung cấp vào bên trái và bên trái. EitherT.pure, lần lượt, bọc giá trị B được cung cấp thành Phải và sau đó vào F.

Một cách hữu ích khác để xây dựng một thể hiện EitherT là sử dụng các phương thức của OptionT toLeft và toRight:

toRight khá giống với phương thức Either.fromOption đã đề cập trước đó: giống như từOptionbuilt một Either từ một Option, toRight tạo EitherT từ một OptionT. Nếu OptionTstores ban đầu Một số giá trị, nó sẽ được bọc thành Phải; mặt khác, giá trị được cung cấp dưới dạng tham số bên trái sẽ được gói vào bên trái.

toLeft là đối tác của toRight, nó bao bọc một số giá trị thành Trái và biến Không có thành Phải đóng lại giá trị bên phải được cung cấp. Điều này ít được sử dụng trong thực tế, nhưng có thể phục vụ, ví dụ: để thực thi kiểm tra tính duy nhất trong mã. Chúng tôi trả về Trái nếu giá trị đã được tìm thấy và Phải nếu nó chưa tồn tại trong hệ thống.

Các phương thức có sẵn trong EitherT khá giống với các phương thức mà chúng tôi đã thấy trong OptionT, nhưng có một số khác biệt đáng chú ý. Ban đầu, bạn có thể gặp một số nhầm lẫn khi nói đến ví dụ: bản đồ. Trong trường hợp của OptionT, điều khá rõ ràng là nên làm gì: bản đồ nên đi qua Tùy chọn kèm theo trong Tương lai, và sau đó ánh xạ Tùy chọn kèm theo. Điều này hơi ít rõ ràng trong trường hợp của EitherT: nó nên ánh xạ trên cả hai giá trị Leftand Right hay chỉ giá trị Right?

Câu trả lời là EitherT thiên vị đúng, do đó bản đồ đơn giản thực sự xử lý giá trị Đúng. Điều này không giống với Either trong thư viện tiêu chuẩn Scala lên tới 2,11, lần lượt không thiên vị: không có bản đồ nào có sẵn trong Either, chỉ dành cho các hình chiếu bên trái và bên phải của nó.

Như đã nói, hãy để nhanh chóng xem xét các phương pháp thiên vị đúng mà EitherT [F, A, B] cung cấp:

Là một lưu ý phụ, cũng có một số phương thức nhất định trong EitherT (mà bạn có thể cần tại một số điểm) ánh xạ qua giá trị Trái, như LeftMap, hoặc trên cả hai giá trị Trái và Phải, như gập hoặc bimap.

EitherT rất hữu ích cho các xác minh chuỗi không nhanh:

Trong ví dụ trên, chúng tôi đã chạy các kiểm tra khác nhau đối với từng mục một. Nếu bất kỳ kiểm tra nào thất bại, EitherT kết quả sẽ chứa giá trị Trái. Mặt khác, nếu tất cả các kiểm tra mang lại Quyền (tất nhiên chúng tôi có nghĩa là Quyền được bọc trong EitherT), thì kết quả cuối cùng cũng sẽ chứa Quyền. Đây là một hành vi không nhanh: chúng tôi đã ngăn chặn một cách hiệu quả luồng hiểu biết ở kết quả Left-ish đầu tiên.

Thay vào đó, nếu bạn tìm kiếm xác thực tích lũy các lỗi (ví dụ: khi xử lý dữ liệu biểu mẫu do người dùng cung cấp), cat.data.Validated có thể là một lựa chọn tốt.

Các vấn đề chung

Nếu bất cứ điều gì không được biên dịch như mong đợi, trước tiên hãy đảm bảo rằng tất cả các hàm ý bắt buộc của Mèo đều nằm trong phạm vi - chỉ cần thử nhập mèo.implicits._ và xem vấn đề còn tồn tại không. Tuy nhiên, như đã đề cập trước đó, nó tốt hơn khi sử dụng nhập khẩu hẹp, nhưng nếu mã không biên dịch thì đôi khi chỉ cần nhập toàn bộ thư viện để kiểm tra xem nó có giải quyết được vấn đề không.

Nếu bạn sử dụng Futures, hãy đảm bảo cung cấp một ExecutContext ẩn trong phạm vi, nếu không, Mèo won đã có thể suy ra các thể hiện ngầm cho các lớp loại Tương lai.

Trình biên dịch có thể khá thường xuyên gặp vấn đề với các tham số kiểu suy ra cho di chuyển ngang và tuần tự. Một cách giải quyết rõ ràng là chỉ định trực tiếp các loại đó, như list.traverse [Tương lai, Đơn vị] (vui nhộn). Tuy nhiên, điều này có thể trở nên khá dài dòng trong một số trường hợp nhất định và cách tốt hơn là thử các phương thức tương đương traverseU và SequU, như list.traverseU (vui vẻ). Họ thực hiện một số thủ thuật cấp độ loại (với cat.Unapply, do đó là U) để giúp trình biên dịch suy ra các tham số loại.

IntelliJ đôi khi báo cáo lỗi trong mã do Mèo tải mặc dù nguồn truyền dưới dạng scalac. Một ví dụ như vậy là các cách gọi của các phương thức của lớp cat.data.Nested, được biên dịch chính xác theo scalac, nhưng kiểm tra kiểu don don trong trình biên dịch trình bày IntelliJ kèm theo. Nó sẽ hoạt động mà không gặp rắc rối theo Scala IDE, mặc dù.

Như một lời khuyên cho việc học tập trong tương lai của bạn: lớp loại Ứng dụng, mặc dù nó có ý nghĩa quan trọng trong chương trình chức năng, nhưng hơi khó hiểu. Theo ý kiến ​​của tôi, nó ít trực quan hơn nhiều so với Functor hoặc Monad, mặc dù nó thực sự đứng ngay giữa Functor và Monad trong hệ thống phân cấp thừa kế. Cách tiếp cận tốt nhất để nắm bắt Ứng dụng là trước tiên hiểu cách sản phẩm (biến đổi F [A] và F [B] thành F [(A, B)]) hoạt động thay vì tập trung vào chính hoạt động ap kỳ lạ.