Functional programing paradigms in modern JavaScript: Pure functions

bản gốc https://hackernoon.com/functional-programming-paradigms-in-modern-javascript-pure-functions-797d9abbee1

JavaScrit đã trở thành một trong những ngôn ngữ lập trình phổ biến nhất cho đến giờ. Nó có thể chạy trên trình duyệt, máy PC, mobile và cả một số thiết bị ngoài nữa. Hơn thế nữa, nó có cả một động đồng năng động và đầy đam mê, điều đó có nghĩa là bạn luôn luôn và luôn luôn có cái hay để học hỏi, để tìm hiểu ^_^

Việc ra mắt bản ES6 có lẽ là bản cập nhật lớn nhất từ trước đến nay mà ngôn ngữ này từng triển khai, các khái niêm như lambdas, classes, generators, destructuring, better syntax, ...。Ngoài ra, một trong những cập nhật ấn tượng nhất có lẽ là Async, nó giúp loại bỏ gánh nặng khi sử udngj callback hoặc promise dự trên các request của async. Một trong những programming paradigms được cộng đồng sử dụng gần đây là functional programming. Chắc hẳn các bạn đã nghe nói đến rất nhiều trong vòng nửa năm trở lại đây.

Bùng nổ hay thoáng qua?

Vậy chúng ta hãy đến với những câu hỏi đầu tiên:

  • functional programming là gì?
  • functional programming đến từ đâu và ai là người sử dụng chúng? Đối với developers, nó có vẻ như một thuật ngữ khá mới mẻ mà họ không tìm thấy lý do để đầu tư thời gian. Trong khi thế giới đang hoàn toàn chấp nhận lập trình hướng đối tượng, tại sao chúng ta không dành thời gian để tiếp nhận một góc nhìn mới về cách xây dựng chương trình?

Trong khi thế giới vẫn sử dụng ngôn ngữ hướng đối tượng, thì việc học tập hay tìm hiểu thêm về một điều gì đó luôn có lội cho skill của bạn. Trên quan điểm của một nhà phát triển, chúng ta không ngừng học hỏi, nhưng đôi lúc để bước tiếp, chúng ta cần phải từ bỏ một số khái niệm. Số lượng các ngôn ngữ lập trinh mới đang gia tăng một cách nhanh chóng và một trong số đó đang thích ứng với các chức năng khác nhau ví dụ như : Scala, Elixir, Elm,.... Nếu bạn là một nhà phát triển JavaScript chắc hẳn bản đã sử dụng Functional Programming trong các ứng dụng của bạn.

Mục đích của series này là làm sáng tỏ các khái niệm và các cách ứng dụng khác nhau của FP. Trong khi đa phần các tutorials hiện tại chỉ là chạy các vị dụ đơn thuần, thì tôi sẽ giúp các bạn tích hợp được những điều học được trong này vào hệ thống.

Functional programming & Pure functions

FP (Functional Programming) là giải pháp xoay quanh ý tưởng rằng chương trình được làm bằng một bộ các chức năng tuần theo một quy tác nhất định. No không có classes, không có kế thừa, cũng không có các mẫu, nó hoàn toàn khác.

Khái niệm chính của FP là ý tưởng về các pure functions. đó là chức năng có một hoặc nhiều tham số đầu vào và có một đầu ra, đặc biệt là nó khong sửa đổi bất kì biến trạng thái nào ngoài phạm vi của chức năng. Mọi function vượt ngòai phạm vi DOM hoặc sử dụng các biến ngoài phạm vi function đều không được coi là pure function và coi như không đủ tiêu chuẩn của FP. Có một câu hỏi là làm sao chương trình nó thể hoạt động được khi toàn bộ function đều là pure function? câu trả lời là đương nhiên là không. Nếu bạn có một chương trình được tạo nên chỉ toàn bằng pure functions thì nó sẽ không có gì thú vị cả, bạn cần có khả năng làm việc với DOM, có khẳ năng gửi request tới services hoặc đọc log trong console. Mục đích của FP là xây dựng chương trình từ những logic rời rạc có thể kết hợp và có thể tái sử dụng, đương nhiên xảy ra side effects là không thể tránh khỏi nhưng bằng cách hạn chế chúng vào những nơi nhất định trong ứng dụng, chugns ta sẽ dễ dàng quản lý và theo dõi chúng.

Tính dự báo (Predictability)

Mục đích của viết những hàm pure function nhỏ chắc là tính dự báo của chúng. Do thực tế là các chức năng của bạn được đống kín nên bạn có thể dễ dàng cho biết đầu ra của nó tùy thuộc đầu vào. Đó cũng là một trong những quy tắc khác trong pure function, nghĩa là khi cho cùng một giá trị đàu vào thì bắt buộc chúng phải cùng trả ra một giá trị đầu ra.

Nếu trước đây bạn chưa từng sử dụng PF trước đây thì bạn có thể phân tích theo trực giác của mình. Bạn có thể thực hiện việc này như một cách thực sự nghiêm ngặc đê rthuwcj thi sự tách biêt các mối liên hệ trong cơ sở hệ thống của bạn. Mỗi phần logic, môi phần chức năng nên được làm chính xác một phần và không can thiếp vào các phần của các mã làm việc khác.

const toCurrency = value =>
  `$${Number(value).toFixed(2)}`

toCurrency(4.56) // $4.56
toCurrency(4.56) // $4.56
toCurrency(4.56) // $4.56
toCurrency(3.28) // $3.28
toCurrency(9,999) // $10.00

Đây là một ví dụ về pure function mà tooid dã được sử dụng một bộ lọc trong một trong những ứng dụng mà tôi đang viết. MỘt cách đơn giản để xác nhận tính năng của bạn thực là pure function bằng cách chạy nó nhiều lần, nếu nó cùng trả ra giá trị đầu ra với các đầu vào giống nhau thì nó được coi là một pure function (một tính chất của pure funciton như đã nói ở trên).

const amount = 4.2890

const toCurrency = () =>
  amount = `$${Number(amount).toFixed(2)}`

toCurrency() // $4.29
toCurrency() // $NaN
toCurrency() // $NaN

Còn ví dụ ngay trên đây không được coi là một pure function và bạn nên tránh viết ra càng nhiều càng tốt. mặc dù nhận được kết quả mà bạn yêu cầu lần đầu tiên bạn chạy nhưng các giá trị về sau bất kì cuộc gọi nào nào đều trả lại giá trị NaN. Hàm như vậy không thể tái sử dụng được trong bất kì kịch bản nào khác trong chương trình của bạn. Mục tiêu lớn hơn của FP như chúng ta sẽ thấy trong các bài viết tiếp theo, là có thể code chương trình của bạn thành từng khối nhỏ để tái sử dụng (các chức năng).

Đề minh họa thêm cách tiếp cận với các mẫu như thế này, tất cả chugns ta hãy em một trong những thư viện yêu thích nhất của cộng đồng hiện nay React. trong React, bạn có thể tạo ra các thành phần phi chức năng -- các thành phàn không làm bất cứ điều gì và được sử dụng để hiển thị một phần của giao diện người dùng mà không cần truy cập vào các lifecycle methods. Các thành phần này được thực hiện bằng cách sử dụng khái niệm pure function, chúng đơn giản là lấy một đầu vào và trả về JSX.

Đây là một ví dụ thực tế của một components mà tôi đã sử dụng:

import React from 'react'

const VideoListItem = ({ video, onVideoSelect }) => {
  const imageUrl = video.snippet.thumbnails.default.url

  return (
    <li onClick={() => onVideoSelect(video)} className='list-group-item'>
      <div className='video-list media'>
        <div className='media-left'>
          <img className='media-object' src={imageUrl} />
        </div>

        <div className='media-body'>
          <div className='media-heading'>{video.snippet.title}</div>
        </div>
      </div>
    </li>
  )
}

export default VideoListItem

Cho dù bạn gọi bao nhiêu lần, chúng sẽ cho kết quả tương tự và trả lại cùng 1 JSX. Bạn có thể bỏ qua cáu trúc thực tế được trả lại nhưng thông báo nó phụ thuộc hoàn toàn vào đầu vào, bao gôm cả achuwcs năng onVideoSelect. Điều này có nghĩa là nó có thể tái sử dụng lại cho nhiều video và nếu tôi muốn xử lý click theo một cách khác tôi chỉ cần pass qua một chức năng khác. Trong khi xem xét chủ đề React, chúng ta hãy cùng suy nghĩ sâu hơn và xem Redux. Khi bạn muốn thay đổi trạng thái trả về trong app của mình, bạn cần thực hiện việc này bằng sử dụng các chức năng được gọi là bộ phần giảm tốc, Mặc dù name, reducers chỉ là các chức năng đơn giản như nhận vào trạng thái hiện tại xong thay đổi mong muốn bạn muoosnt hực hiện và trả lại trạng thái mới. Điều này được tính là pure, phải không?

import { FETCH_WEATHER } from '../actions/index'

export default function (state = [], action) {
  switch (action.type) {
    case FETCH_WEATHER:
      return [ ...state, action.payload.data ]
    default:
      return state
  }
}

Reducers được sử dụng để mô tả tất cả các thay đổi đơn trong trạng thái ứng dụng của bạn và thực hiện truy cập theo các cách của chức năng, mà không can thiệp hay sửa đổi các biến global. Nó chỉ hoạt động thông qua những gì được truyền vào, do đó bạn biết rằng các giá trị nhất định đầu vào cho reducer, sau đó, app của bạn sẽ xem xét một số giải pháp nhất định. ĐIều này sẽ giúp cho dễ dàng dự đoán để gỡ lỗi và thử nghiệm như chúng ta sẽ thấy trong các bài sau.

khép kín (self contained)

Giờ chúng ta hãy xem một quy tắc cần thiết khác để viết pure functions một cách thích hợp, họ không sửa đổi vị trí gọi hàm. Điều này có nghĩa là bạn không nên sửa đổi bất kì thuộc tính nào trên đối tượng này. Điều bí ẩn này của JavaScript đã tạo ra rất nhiều sự ngạc nhiên và hụt hẫng cho các nhà phát triển trên toàn thế giới. Thông thường, mỗi khi bạn phụ vào function bạn phải theo dõi và chú y s đến cách thức và nơi chức năng của bạn được gọi, Nhung trong FP, thay đổi vị trí gọi hàm là không thể chấp nhận dược và được coi là một side effect.

Có thể bạn đã đang sử dung pure function trong hệ thống của mình nhưng chưa thực sự hiểu rõ nó. Có rất nhiefu chức năng được xây dựng trong cốt lỗi của JavaSCript theo các mô hình như các chức năng trên nguyên mẫu của đối tượng String

const hello = 'Hello ';
const greeting = hello.concat('World'); // Hello World

hello // Hello
greeting // Hello World

bạn có thể nhìn vào concat trong hàm này. bạn có thể thấy, nó không làm thay đổi chức năng khi nó được gọi vào. Nếu bạn chưa quen với ngôn ngữ bạn có thể mong đợi biến hello có giá trị "Hello World". Tuy nhiên, vì chức năng concat tuân thủ các tiêu chuẩn của FP, biến mà nó được thực hiện vẫn không bị ảnh hưởng. Trong thực tế, nếu bạn muốn truy cập vào giá trị trả về từ concat, bạn cần gán nó cho một biến khác hoặc trực tiếp truyền nó tới một cái gì đó. Có các chức năng khác trong JavaScript được sử dụng rộng rãi trong lập trình chức năng. Đó là những phương pháp trên nguyên mẫu Array, cụ thể hơn - map, filter và reduce.

const numbers = [1, 4, 6, 9]
const byTwo = numbers.map(x => x * 2)

numbers // [1, 4, 6, 9]
byTwo // [2, 8, 12, 18]

Giống như trong ví dụ trước, mảng số sẽ không bị ảnh hưởng. Chức năng bản đồ sẽ trả về một mảng hoàn toàn mới. Tuy nhiên, có điều thú vị hơn - họ là những ví dụ của cái gọi là Higher Order Function. Higher Order Function là các chức năng có thể lấy một hàm như đầu vào hoặc trả về một hàm như đầu ra. Như bạn thấy trong ví dụ, chúng ta không truyền một giá trị, chúng ta sẽ truyền một hàm để được thực hiện trên mỗi phần của mảng. Đây là điều khác cần lưu ý - chức năng là class đầu tiên! Chúng có thể được chuyển qua lại như là đầu vào hoặc trả lại như là kết quả của một hàm. Điều này kết hợp với thực tế là họ sẽ trả lại một mảng hoàn toàn mới thay vì sửa đổi và cho phép gọi chuỗi hiệu quả tốt hơn:

const numbers = [1, 2, 4, 5, 6, 7, 7, 9, 11, 14, 43, 56, 89]

function isEven(x) {
  return x % 2 === 0
}

function addTwo(x) {
  return x * 2
}

const result = numbers.filter(isEven).map(addTwo) // [4, 8, 12, 28, 112]

Những gì chúng tôi trình bày trong phần này gợi ý ở các phần khác của FP - tính không thay đổi và thành phần chức năng. Để không làm cho bài viết này trở nên quá phức tạp, tôi sẽ cố ý bỏ qua chúng.

Testing

Hãy nhìn vào các pure functions từ phía đảm bảo chất lượng của sự vật. Chúng ta đều biết rằng các bài test đều quan trọng và rất có lợi nhưng chúng ta không nhất thiết phải luôn luôn làm điều đó. Bằng cách sử dụng functional approach bạn tránh side effect và functions của bạn không truy cập hoặc sửa đổi bất cứ điều gì từ phạm vi global. Điều này loại bỏ gánh nặng của việc gia tăng tính phức tạp- tất cả mọi thứ mà chức năng của bạn cần được truyền một đầu vào và bạn chỉ cần xác nhận đầu ra là chính xác. Kiểm tra chức năng của bạn chỉ trở nên dễ dàng hơn rất nhiều. Trong thực tế, bạn có thể xem các trường hợp thử nghiệm của bạn giống như kiểm tra sanity. Khi các khối xây dựng ứng dụng của bạn có khép kín và không làm bất cứ điều gì bất thường, bạn chỉ cần đảm bảo rằng mọi thứ đều như thế. Thử nghiệm sẽ trở thành một quá trình kiểm tra. Bạn có một danh sách các câu hỏi (các thông số đầu vào của bạn) và một danh sách các câu trả lời (các tham số đầu ra của bạn) - bây giờ bạn chỉ cần so sánh các câu trả lời của chức năng so với các câu trả lời thực tế. Bất cứ ai trong bộ phận QA đều sẽ rất thoái mái nếu bạn đã viết mã như thế. Mỗi ứng dụng xây dựng bằng cách sử dụng một mô hình Redux sẽ dễ dàng hơn nhiều để thử nghiệm hơn so với những người không. Giống như tôi đã đề cập ở phần trên, mọi thay đổi trong trạng thái ứng dụng của bạn được mô tả như một hàm thuần túy có đầu ra có thể dự đoán dựa trên đầu vào của nó. Đây là điều đơn giản nhất để kiểm tra - chúng ta chỉ cần gọi hàm với một số giá trị. Trong ví dụ dưới đây tôi đang thử nghiệm một hành vi reducer bằng cách hành động mà ông ta không nên đáp ứng và người khác mà ông ta nên làm. Trong cả hai trường hợp, nhiều cuộc gọi với cùng một tham số sẽ cung cấp cho cùng một đầu ra.

import { expect } from '../test_helper'
import commentReducer from '../../src/reducers/commentsReducer'
import { SAVE_COMMENT } from '../../src/actions/types'

describe('comments reducer', () => {
  
  it('handles action with unknown type', () => {
    const state = commentReducer([], { type: 'UNKNOWN_ACTION' })
    expect(state).to.be.instanceOf(Array)
  })
  
  it('handles action of type SAVE_COMMENT', () => {
    const action = {
      type: SAVE_COMMENT,
      payload: 'Test comment'
    }

    expect(commentReducer([], action)).to.eql(['Test comment'])
  })
})

What's next Functional Programming có thể là một chủ đề khá lúng túng và khó hiểu nhưng để bắt đầu cảm thấy thoải mái chúng ta cần một sự hiểu biết vững chắc về các building blocks - các function. Trong bài này chúng tôi đã giới thiệu cho các ứng dụng và lợi ích của việc sử dụng các pure functions trong cơ sở mã nguồn của bạn. Trong các bài tiếp theo, chúng ta sẽ đi sâu hơn một bậc với những thứ như tính không thay đổi và thành phần chức năng.

Chân thành cám ơn các bạn đã đọc đến cuối bài


All Rights Reserved