Pattern Matching in Swift

Introduction

Pattern matching là một tính năng mạnh mẽ ở bất kì ngôn ngữ lập trình nào, bởi vì nó cho phép bạn có thể thiết kế ra được các rules để match được cái giá trị, Pattern matching làm cho code trở lên linh hoạt và đơn giản. Trong bài viết này chúng ta sẽ tìm hiểu các pattern sau:

  • Tuple pattern
  • Type-casting patterns
  • Wildcard pattern
  • Optional pattern
  • Enumeration case pattern
  • Expression pattern

Để có thể thấy được sự tiện ích của pattern matching, trong tutorial này chúng ta sẽ sử dụng pattern matching để schedule và publish các tutorials trên trang raywenderlick.com. Chú ý: tutorial này yêu cầu Xcode 8 và Swift 3, người đọc yêu cầu phải có kiến thức cơ bản về Swift.

Download : https://koenig-media.raywenderlich.com/uploads/2016/07/starter-project.playground-2.zip Sau đó mở file starter project.playground bằng Xcode File play ground này gồm 2 thành phần:

  • Hàm random_uniform(value:) tạo ngày ngẫu nhiên cho việc lập lịch.
  • Code đọc thông tin file tutorials.json trả về thông tin lập lịch các bài viết.

Mỗi bài tutorial sẽ được lập lịch gồm 2 thuộc tính: title và scheduled day. Mỗi ngày chỉ được publish 1 bài tutorial, xem lại thông tin của file tutorials.json chúng ta sẽ thấy có 2 tutorials được lập lịch publish cùng 1 ngày. Chúng ta cần sửa nó sử dụng Pattern matching.

Pattern Matching Types

Các loại Pattern Matching:

  • Tuple patterns được sử dụng để match giá trị kiểu tuple.
  • Type-casting patterns cho phép bạn cast hoặc match types.
  • Wildcard patterns dùng để match và ignore các loại giá trị khác nhau.
  • Optional patterns sử dụng để match các giá trị optional.
  • Enumeration case patterns sử dụng để match kiểu enumeration.
  • Expression patterns cho phép so sánh các giá trị vơi các biểu thức.

Chúng ta sẽ sử dụng tất cả các pattern này trong bài viết này.

Tuple Pattern

Đầu tiên chúng ta sẽ tạo tupple pattern để tạo mảng các tutorials. Trong file playground, thêm đoạn code sau


enum Day: Int {
  case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}

Đoạn code trên sẽ tạo ra enumeration cho các ngày trong tuần. Giá trị raw là kiểu Int nên các ngày sẽ được gán giá trị từ 0-6 tương ứng thứ 2 - thứ 6

Thêm đoạn code dưới đây vào sau phần khai báo enumeration:

class Tutorial {

  let title: String
  var day: Day?

  init(title: String, day: Day? = nil) {
    self.title = title
    self.day = day
  }
}

Ở đây chúng ta đã định nghĩa kiểu tutorial với 2 thuộc tính: title, scheduled day. day là kiểu optional vì nó có thể nil trong các trường hợp tutorials chưa được lập lịch. Chúng ta implement CustomStringConvertible để có thể dễ dàng in ra giá trị tutorial:

extension Tutorial: CustomStringConvertible {
  var description: String {
    var scheduled = ", not scheduled"
    if let day = day {
      scheduled = ", scheduled on \(day)"
    }
    return title + scheduled
  }
}

Bây giờ chúng ta sẽ tạo mảng để lưu các giá trị tutorial này:

var tutorials: [Tutorial] = []

Tiếp theo, thêm đoạn code sau vào cuối file playground để chuyển mảng các dictionaries sang mảng các đối tượng tutorials

for dictionary in json {
  var currentTitle = ""
  var currentDay: Day? = nil

  for (key, value) in dictionary {
    // todo: extract the information from the dictionary
  }

  let currentTutorial = Tutorial(title: currentTitle, day: currentDay)
  tutorials.append(currentTutorial)
}

Ở đây chúng ta sử dụng tupple pattern, trong vòng lặp for in sử dụng tupple (key, value) để lấy ra giá trị từ dictionary. Tiếp theo chúng ta sẽ thiết lập các giá trị này vào thuộc tính của tutorial bằng cách sử dụng Type-Casting Patterns Type-Casting Patterns Thêm đoạn code sau vào trong vòng lặp (key, value) ở trên

// 1
switch (key, value) {
  // 2
  case ("title", is String):
    currentTitle = value as! String
  // 3
  case ("day", let dayString as String):
    if let dayInt = Int(dayString), let day = Day(rawValue: dayInt - 1) {
      currentDay = day
  }
  // 4
  default:
    break
}

Các bước lần lượt như :sau Sử dụng câu lệnh: switch (key, value) => tupple pattern được sử dụng lần nữa. Chúng ta kiểm tra xem thuộc tính title của tutorial có phải là kiểu String hay không sử dụng type-casting pattern, sau đó chúng ta ép kiểu nếu điều kiện trên là đúng. Tương tự với thuộc tính day của tutorial, chúng ta cũng sử dụng type-casting pattern để kiểm tra và ép kiểu cho giá trị này. Thêm dòng lệnh print vào cuối file playground để in ra thông tin của tutorials.

print(tutorials)

Nhìn vào màn hình console chúng ta sẽ thấy được mỗi một tutorial trong mảng có 1 giá trị title và scheduled day tương ứng. Mọi thứ đã được thiết lập, bây giờ chúng ta sẽ thực hiện việc lập lịch sao cho mỗi ngày chỉ publish một tutorial.

Wildcard Pattern Chúng ta sẽ sử dụng wildcard pattern để lập lịch cho các bài tutorials này, tuy nhiên chúng ta cần unscheduled chúng trước:

tutorials.forEach { $0.day = nil }

Đoạn code trên sẽ unscheduled tất cả các tutorial bằng cách thiết lập giá trị day cho các tutorial này = nil. Để lập lịch cho các tutorial them đoạn code sau vào cuối file playground:

// 1
let days = (0...6).map { Day(rawValue: $0)! }
// 2
let randomDays = days.sorted { _ in random_uniform(value: 2) == 0 }
// 3
(0...6).forEach { tutorials[$0].day = randomDays[$0] }

Let’s break it down: Đầu tiên chúng ta tạo mảng days, mỗi ngày của một tuần sẽ xảy ra đúng 1 lần. Sắp xếp lại mảng. Hàm random_uniform(value:) sử dụng để ngẫu nhiên quyết định một element sẽ được sắp xếp trước hay sau element tiếp theo ở trong mảng. Cuối cùng, chúng ta gán 7 tutorials đầu tiền với các giá trị ngẫu nghiên này. Thêm dòng code sau:

print(tutorials)

Quan sát, chúng ta sẽ thấy được các tutorial đã được thiết lập lịch cho mỗi 1 ngày trong tuần, và không có trường hợp lặp như ban đầu.

Optional Pattern Để sắp xếp mảng tutorials theo thứ tự ngày :

// 1
tutorials.sort {
  // 2
  switch ($0.day, $1.day) {
    // 3
    case (nil, nil):
      return $0.title.compare($1.title, options: .caseInsensitive) == .orderedAscending
    // 4
    case (let firstDay?, let secondDay?):
      return firstDay.rawValue < secondDay.rawValue
    // 5
    case (nil, let secondDay?):
      return true
    case (let firstDay?, nil):
      return false
  }
}

Step-by-step: Chúng ta sắp xếp mảng tutorials sử dụng hàm sort(_😃. Bởi vì giá trị của day là optional, do đó trong trường hợp này ta phải sử dụng opational pattern để so sánh các giá trị này để thực hiện việc sắp xếp mảng.

print(tutorials)

In ra ta thấy tutorials đã được sắp xếp như mong muốn.

Enumeration Case Pattern Tiếp theo ta sẽ sử dụng enumeration case pattern để quyết định tên của ngày lập lịch cho mỗi tutorial. Thêm đoạn code sau:

extension Day {

  var name: String {
    switch self {
      case .monday:
        return "Monday"
      case .tuesday:
        return "Tuesday"
      case .wednesday:
        return "Wednesday"
      case .thursday:
        return "Thursday"
      case .friday:
        return "Friday"
      case .saturday:
        return "Saturday"
      case .sunday:
        return "Sunday"
    }
  }
}

Việc match các giá trị hiện tại (self) với các giá trị có thể từ enumeration ở đây chính là enumeration pattern. Thật ấn tượng, rignt? Expression Pattern Tiếp theo chúng ta sẽ them thuộc tính để mô tả các yêu cầu lập lịch của các tutorial. Chúng ta có thể sử dụng enumeration case pattern lần nữa như bên dưới (không them vào code):

var order: String {
  switch self {
    case .monday:
      return "first"
    case .tuesday:
      return "second"
    case .wednesday:
      return "third"
    case .thursday:
      return "fourth"
    case .friday:
      return "fifth"
    case .saturday:
      return "sixth"
    case .sunday:
      return "seventh"
  }
}

Tuy nhiên việc làm một thứ mà lặp lại nhiều lần thì không hay. Do đó thay vào đó, chúng ta sử dụng phương cách khác đó là sử dụng expression pattern. Đầu tiên chúng ta cần overload pattern matching operator để thay đổi các chức năng mặc định và làm chon ó có thể làm việc với tất cả các ngày. Thêm đoạn code sau vào cuối file playground.

func ~=(lhs: Int, rhs: Day) -> Bool {
  return lhs == rhs.rawValue + 1
}

Đoạn code này cho phép bạn có thể match days tới intergers: 1-7

Thêm đoạn code sau :

extension Tutorial {

  var order: String {
    guard let day = day else {
      return "not scheduled"
    }
    switch day {
      case 1:
        return "first"
      case 2:
        return "second"
      case 3:
        return "third"
      case 4:
        return "fourth"
      case 5:
        return "fifth"
      case 6:
        return "sixth"
      case 7:
        return "seventh"
      default:
        fatalError("invalid day value")
    }
  }
}

Nhờ vào overloaded pattern matching operator, đối tượng day bây giờ có thể matched được với biển thức integer. Đây chính là expression pattern in action.

Putting It All Together Bây giờ chúng ta đã định nghĩa được tên các ngày và thứ tự của các tutorial. Chúng ta có thể in ra status của các tutorial này. Thêm đoạn code sau vào cuối file playground:

for (index, tutorial) in tutorials.enumerated() {
  guard let day = tutorial.day else {
    print("\(index + 1). \(tutorial.title) is not scheduled this week.")
    continue
  }
  print("\(index + 1). \(tutorial.title) is scheduled on \(day.name). It's the \(tutorial.order) tutorial of the week.")
}

Ở vòng lặp for-in chúng ta lại sử dụng tupple pattern một lần nữa. Ok, vậy chúng ta đã tìm hiểu được một loạt pattern matching trong Swift. Tài liệu tham khảo: https://www.raywenderlich.com/134844/pattern-matching-in-swift