Scala Try – Xử lý lỗi

Trong lập trình một ngôn ngữ, chúng ta sẽ không bao giờ có thể tránh khỏi lỗi xảy ra ở trong chương trình dù nguyên nhân lỗi đó có thể là do khách quan mà chúng ta thậm trí không kiểm soát được. Các ngôn ngữ đều hỗ trợ chúng ta kiểm soát những lỗi không mong muốn này và tất nhiên Scala cũng vậy.

Scala giúp chúng ta xử lý và kiểm soát các lỗi này một cách rất tự nhiên. Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu cách tiếp cận xử lý các lỗi dựa trên kiểu Try.

Throw và Catch các Exception

Trước khi chúng ta đi tìm hiểu cách tiếp cận của Scala trong việc xử lý các lỗi, chúng ta hãy cùng xem qua cách tiếp cận mà chúng ta sử dụng tương đối phổ biến trong các ngôn ngữ khác khi làm việc với các điều kiện lỗi. Giống như các ngôn ngữ khác, Scala cũng cho phép chúng ta throw một Exception

case class Customer(age: Int)
class Cigarettes
case class UnderAgeException(message: String) extends Exception(message)
def buyCigarettes(customer: Customer): Cigarettes =
  if (customer.age < 16)
    throw UnderAgeException(s"Customer must be older than 16 but was ${customer.age}")
  else new Cigarettes

Khi throw các Exception, chúng ta có thể dễ dàng Catch và xử lý tương tự như trong Java, sử dụng một hàm partial để xác định các exception mà chúng ta muốn xử lý. Tương tự như thế, try/catch của Scala là một expression, nên theo như đoạn code bên dưới đây chúng ta sẽ trả về thông báo lỗi

val youngCustomer = Customer(15)
try {
  buyCigarettes(youngCustomer)
  "Yo, here are your cancer sticks! Happy smokin'!"
} catch {
    case UnderAgeException(msg) => msg
}

Xử lý lỗi theo cách Functional

Với cách xử lý lỗi như bên trên, chúng ta có thể dễ dàng thấy rằng nó sẽ khiến cho code trở nên bẩn thỉu rất nhanh và nó thực sự không hợp lý với Functional Programming. Nó cũng không tốt đối với các ứng dụng có nhiều xử lý đồng thời. Ví dụ nếu bạn cần xử lý một Throw Exception trong một Actor mà được chạy trong các thread khác nhau, bạn chắc chắn sẽ không thể xử lý catch Exception – Bạn chắc chắn sẽ mong muốn có thể nhận được thông báo lỗi khi có một điều kiện lỗi xảy ra.

Do đó trong Scala, nó thường hiển thị một lỗi xảy ra bằng cách trả về một giá trị thích hợp từ hàm của bạn. Scala sử dụng một kiểu riêng đại diện cho các tính toán với kết quả trả về là một Exception.

Thuật ngữ Try

Thuật ngữ Try là cách tốt nhất để giải thích khi so sánh nó với kiểu Option

Option[A] là một container cho giá trị của kiểu A mà nó có thể tồn tại hoặc không. Try[A] biểu diễn cho một tính toán mà kết quả có thể là một giá trị kiểu A nếu không có lỗi gì xảy ra, hoặc là một vài Throwable nếu có lỗi gì đó xảy ra. Trường hợp trả về như một kiểu container mục đích để các lỗi có thể xảy ra dễ dàng được truyền qua lại giữa các phần đang thực hiện chạy bất đồng bộ trong ứng dụng.

Có hai kiểu Try khác nhau: Nếu một instace của Try[A] đại diện cho một tính toán thành công, thì nó là một instace của Success[A], đơn giản là đóng gói giá trị của kiểu A. Ngoài ra nó đại diện cho một tính toán xảy ra lỗi, nó là instace của Failure[A], đóng gói một Throwable ví dụ một Exception hoặc các kiểu lỗi khác.

Nếu chúng ta biết rằng một tính toán có thể xảy ra lỗi, chúng ta chỉ cần đơn giản sử dụng Try[A] như một kiểu trả về cho hàm chúng ta. Điều này giúp cho nó trở nên rõ ràng hơn và bắt buộc các client sử dụng hàm phải xử lý trong một vài trường hợp lỗi có thể xảy ra.

Ví dụ chúng ta có thể viết một hàm dùng để tải một trang web về. Người dùng có thể nhập vào một URL của trang web mà họ muốn tải về. Hàm sẽ thực hiện phân tích Url nhập vào và tạo một java.net.URL từ đấy

import scala.util.Try
import java.net.URL
def parseURL(url: String): Try[URL] = Try(new URL(url))

Như bạn nhìn thấy nó sẽ trả về một giá trị của kiểu Try[URL]. Nếu url truyền vào có format chuẩn, thì nó sẽ trả về một Success[URL]. Nếu URL throw một MalformedURLException, thì nó sẽ trả về một Failure[MalformedURLException].

Để thực hiện điều này, chúng ta đang sử dụng phương thức factory apply trên Try là một companion object. Phương thức này nhận đầu vào là tên của tham số với kiểu là A ở đây là URL. Trong ví dụ của chúng ta, điều này nghĩa là new URL(url) được thực hiện bên trong phương thức apply của Try object.  Trong phương thức, các Exception được bắt và trả về một Failure chứa Exception tương ứng.

Do đó, parseURL("https://itexpertvn.com") sẽ trả về một Success[URL] chứa URL được tạo, còn parseURL("url") sẽ trả về một Failure[URL] chứa một MalformedURLException

Giá trị của Try

Làm việc với các instance của Try chắc chắn là rất giống với với các giá trị của Option, nên bạn sẽ không thấy điều gì mới mẻ ở đây.

Bạn có thể kiểm tra nếu một Try trả về thành công bằng cách gọi phương thức isSuccess của nó và sau đó lấy giá trị được đóng gói bằng cách gọi hàm get. Nhưng tất nhiên là trong thực tế sẽ không có nhiều trường hợp mà chúng ta cần xử lý như trên

Nó cũng có thể sử dụng getOrElse  để truyền giá trị mặc định được trả về nếu Try là một Failure

val url = parseURL(Console.readLine("URL: ")) getOrElse new URL("https://default-url.com")

Nếu URL truyền vào bởi người dùng là sai format, chúng ta sẽ sử dụng default-url.com như một dự phòng thay thế

Các hoạt động

Một trong những đặc điểm quan trọng của Try đó là giống như Option nó hỗ trợ tất các các phương thức higher-order mà bạn biết sử dụng trong các kiểu collection.

Mapping và flat maping

Mapping một Try[A] mà là một Success[A] thành một kết quả Try[B] trong một Success[B]. Nếu nó là một Failure[A], kết quả Try[B] sẽ là một Failure[B], ngoài ra chứa Exception giống với Failure[A].

parseURL("https://itexpertvn.com").map(_.getProtocol)
// Kết quả là Success("https")
parseURL("garbage").map(_.getProtocol)
// Kết quả là Failure(java.net.MalformedURLException: no protocol: garbage)

Nếu chúng ta sử dụng một chuỗi các hoạt động map, kết quả trả về sẽ là nhiều Try lồng vào nhau mà bạn không mong muốn. Hãy cùng xem ví dụ sau trả về một InputStream cho URL truyền vào

import java.io.InputStream
def inputStreamForURL(url: String): Try[Try[Try[InputStream]]] = parseURL(url).map { u =>
  Try(u.openConnection()).map(conn => Try(conn.getInputStream))
}

Vì các hàm anonymous được truyền để 2 map gọi mỗi lần trả về một Try và kiểu trả về là một Try[Try[Try[InputStream]]]

Đây là nơi mà bạn có thể sử dụng flatMap một Try có ích. Phương thức flatMap trên Try[A] được mong đợi để một hàm được truyền sẽ nhận được A và trả về một Try[B]. Nếu instace Try[A] của chúng ta là một Failure[A], lỗi này sẽ được trả về như một Failure[B]. Nếu Try[A] là một Success[A], flatMap mở giá trị A trong nó và map nó thành Try[B] bằng cách truyền giá trị này đến hàm map.

Điều này nghĩa là chúng ta có thể tạo một chuỗi các hoạt động mà yêu cầu các giá trị thực hiện trên Success instance bằng cách kết nối một số lượng các cuộc gọi flatMap tuỳ ý. Bất ký Exception nào xảy ra trong quá trình xử lý được đóng gói trong một Failure, nghĩa là kết quả cuối cùng cho chuỗi các hoạt động này cũng là một Failure.

Chúng ta hãy thử viết lại phương thức inputStreamForURL trong ví dụ bên trên sử dụng flatMap

def inputStreamForURL(url: String): Try[InputStream] = parseURL(url).flatMap { u =>
  Try(u.openConnection()).flatMap(conn => Try(conn.getInputStream))
}

Bây giờ, chúng ta có thể nhận được một Try[InputStream] mà có thể đóng gói một Failure của một Exception khi có bất kỳ một throw Exception nào xảy ra, hay một Success đóng gói trực tiếp InputStream, kết quả cuối cùng của một chuỗi các hoạt động.

Filter và foreach

Tất nhiên là chúng ta cũng có thể thực hiện lọc một Try hoặc gọi phương thức foreach. Cả 2 đều hoạt động chính xác giống như chúng ta sử dụng Option

Phương thức filter sẽ trả về một Failure nếu Try được gọi là một Failure hoặc nếu điều kiện lọc trả về false (trong trường hợp này nó sẽ đóng gói một Exception là NoSuchElementException). Nếu Try được gọi là một Success và điều kiện lọc là true, Success được trả về sẽ không thay đổi.

def parseHttpURL(url: String) = parseURL(url).filter(_.getProtocol == "http")
parseHttpURL("https://apache.openmirror.de") // kết quả là Success[URL]
parseHttpURL("ftp://mirror.netcologne.de/apache.org") // kết quả là Failure[URL]

Hàm được truyền đến foreach chỉ được chạy nếu Try là một Success, cho phép bạn thực hiện một side-effect. Hàm được truyền đến foreach chỉ chạy đúng một lần trong trường hợp này

parseHttpURL("https://itexpertvn.com").foreach(println)

For comprehension

Hỗ trợ map, flatMap, foreach và filter nghĩa rằng chúng ta cũng có thể sử dụng For comprehension mục đích để xử lý một chuỗi các hoạt động trên  Try. Thông thường sử dụng For thì code đọc sẽ dễ hiểu hơn. Để chứng minh điều này chúng ta hãy cùng xem ví dụ sau đây, tạo một phương thức trả về nội dung của trang web với URL nhập vào sử dụng for comprehension

import scala.io.Source
def getURLContent(url: String): Try[Iterator[String]] =
  for {
    url <- parseURL(url)
    connection <- Try(url.openConnection())
    is <- Try(connection.getInputStream)
    source = Source.fromInputStream(is)
  } yield source.getLines()

Có 3 phần mà có thể xảy ra lỗi, tất cả đều được chúng ta kiểm soát bằng cách sử dụng kiểu Try.

Đầu tiên chúng ta đã tạo phương thức parseURL trả về một Try[URL]. Chỉ khi nó trả về Success[URL], chúng ta sẽ thử mở một kết nối dùng openConnection và tạo một InputStream mới từ nó sử dụng getInputStream. Nếu mở một kết nối và tạo InputStream thành công, nó sẽ tiếp tục chạy tiếp, cuối cùng sử dụng yield để lầy các hàng của trang web sử dụng getLine. Bởi vì chúng ta thực hiện một chuỗi các cuộc gọi nên hàm flatMap được gọi trong for comprehension. Kiểu của kết quả trả về là “phẳng” Try[Iterator[String]]

Pattern matching

Trong một vài thời điểm trong code của bạn, bạn thường mong muốn được biết liệu Try instance mà bạn đã nhận được như là kết quả của các tính toán trả về thành công hoặc lỗi xảy ra và thực hiện các nhánh source code khác nhau dựa trên kết quả. Thông thường chúng ta sẽ sử dụng pattern matching trong trường hợp này. Điều này có thể thực hiện dễ dàng bởi vì cả SuccessFailure đều là case class.

Chúng ta muốn render ra một trang web nếu nó có thể được lấy, hoặc in ra một thông tin lỗi nếu không thể

import scala.util.Success
import scala.util.Failure
getURLContent("http://danielwestheide.com/foobar") match {
  case Success(lines) => lines.foreach(println)
  case Failure(ex) => println(s"Problem rendering URL content: ${ex.getMessage}")
}

Recover từ Failure

Nếu bạn muốn thiết lập một vại hoạt động mặc định trong trường hợp một Failure, bạn không phải sử dụng getOrElse. Sử dụng recover thay thế, mà dự đoạn một partial function và trả về một Try khác. Nếu recover được gọi trên một Success instance, instance đó sẽ được trả về. Ngoài ra nếu partial function được định nghĩa giống với Failure instace đang xảy ra, kết qủa của nó được trả về như một Success

Hãy cùng xem ví dụ bên dưới đây

import java.net.MalformedURLException
import java.io.FileNotFoundException
val content = getURLContent("garbage") recover {
  case e: FileNotFoundException => Iterator("Requested page does not exist")
  case e: MalformedURLException => Iterator("Please make sure to enter a valid URL")
  case _ => Iterator("An unexpected error has occurred. We are so sorry!")
}

Chúng ta có thể gọi .get một cách an toàn vì trong mọi trường hợp cả khi lỗi xảy ra các giá trị đều được đóng gói trả về trong Try[Interator[String]] được gán vào content. Thử gọi content.get.foreach(println) kết quả là Please make sure to enter a valid URL được in ra trên console

 

 

 

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.