Làm cách nào tôi có thể _read_ mã JavaScript chức năng?


9

Tôi tin rằng tôi đã học được một số / nhiều / hầu hết các khái niệm cơ bản về lập trình chức năng trong JavaScript. Tuy nhiên, tôi gặp khó khăn khi đọc mã chức năng, thậm chí mã tôi đã viết và tự hỏi liệu có ai có thể cho tôi bất kỳ gợi ý, mẹo, thực tiễn tốt nhất, thuật ngữ, v.v. có thể giúp ích không.

Lấy mã dưới đây. Tôi đã viết mã này. Nó nhằm mục đích gán một sự tương đồng phần trăm giữa hai đối tượng, giữa nói {a:1, b:2, c:3, d:3}{a:1, b:1, e:2, f:2, g:3, h:5}. Tôi đã tạo mã để trả lời câu hỏi này trên Stack Overflow . Bởi vì tôi không chắc chắn chính xác loại phần trăm mà poster đã hỏi về, nên tôi đã cung cấp bốn loại khác nhau:

  • phần trăm các khóa trong đối tượng thứ 1 có thể được tìm thấy trong phần 2,
  • phần trăm của các giá trị trong đối tượng thứ 1 có thể được tìm thấy trong phần 2, bao gồm các mục trùng lặp,
  • phần trăm của các giá trị trong đối tượng thứ 1 có thể được tìm thấy trong phần 2, không cho phép trùng lặp và
  • tỷ lệ phần trăm của các cặp {key: value} trong đối tượng thứ 1 có thể tìm thấy trong đối tượng thứ 2.

Tôi bắt đầu với mã bắt buộc hợp lý, nhưng nhanh chóng nhận ra rằng đây là một vấn đề rất phù hợp cho lập trình chức năng. Cụ thể, tôi nhận ra rằng nếu tôi có thể trích xuất một hoặc ba hàm cho mỗi trong bốn chiến lược trên đã xác định loại tính năng tôi đang tìm cách so sánh (ví dụ: các khóa hoặc giá trị, v.v.), thì tôi có thể có thể giảm (bỏ qua cách chơi chữ) phần còn lại của mã thành các đơn vị lặp lại. Bạn biết đấy, giữ cho nó KHÔ. Thế là tôi chuyển sang lập trình chức năng. Tôi khá tự hào về kết quả, tôi nghĩ rằng nó khá thanh lịch và tôi nghĩ tôi hiểu những gì tôi đã làm khá tốt.

Tuy nhiên, ngay cả khi đã tự viết mã và hiểu từng phần của nó trong quá trình xây dựng, khi tôi nhìn lại nó, tôi tiếp tục gặp khó khăn hơn cả về cách đọc bất kỳ nửa dòng cụ thể nào, cũng như cách "Grok" những gì bất kỳ nửa dòng mã cụ thể đang thực sự làm. Tôi thấy mình đang tạo ra những mũi tên tinh thần để kết nối những phần khác nhau nhanh chóng biến thành một mớ mì spaghetti.

Vì vậy, bất cứ ai cũng có thể cho tôi biết làm thế nào để "đọc" một số đoạn mã phức tạp hơn theo cách vừa súc tích vừa góp phần vào sự hiểu biết của tôi về những gì tôi đang đọc? Tôi đoán những phần khiến tôi thích thú nhất là những phần có nhiều mũi tên béo liên tiếp và / hoặc những phần có nhiều dấu ngoặc đơn liên tiếp. Một lần nữa, ở cốt lõi của chúng, cuối cùng tôi cũng có thể tìm ra logic, nhưng (tôi hy vọng) có một cách tốt hơn để đi nhanh chóng và rõ ràng và trực tiếp "tiếp nhận" một dòng lập trình JavaScript chức năng.

Vui lòng sử dụng bất kỳ dòng mã nào từ bên dưới, hoặc thậm chí các ví dụ khác. Tuy nhiên, nếu bạn muốn một số gợi ý ban đầu từ tôi, đây là một vài. Bắt đầu với một cách hợp lý đơn giản. Từ gần cuối mã, có cái này được truyền dưới dạng tham số cho hàm : obj => key => obj[key]. Làm thế nào để một người đọc và hiểu điều đó? Một ví dụ dài hơn là một hàm đầy đủ từ gần đầu : const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. Phần cuối cùng maplàm cho tôi đặc biệt.

Xin lưu ý, tại thời điểm này tôi không tìm kiếm tài liệu tham khảo về Haskell hoặc ký hiệu trừu tượng tượng trưng hoặc các nguyên tắc cơ bản của cà ri, v.v. Điều tôi đang tìm kiếm là những câu tiếng Anh mà tôi có thể im lặng trong khi nhìn vào một dòng mã. Nếu bạn có tài liệu tham khảo cụ thể giải quyết chính xác điều đó, thật tuyệt, nhưng tôi cũng không tìm kiếm câu trả lời rằng tôi nên đọc một số sách giáo khoa cơ bản. Tôi đã làm điều đó và tôi nhận được (ít nhất là một lượng đáng kể) logic. Cũng lưu ý, tôi không cần câu trả lời thấu đáo (mặc dù những nỗ lực như vậy sẽ được hoan nghênh): Ngay cả những câu trả lời ngắn cung cấp cách đọc thanh lịch cho một dòng cụ thể của mã rắc rối khác sẽ được đánh giá cao.

Tôi cho rằng một phần của câu hỏi này là: Tôi thậm chí có thể đọc mã chức năng một cách tuyến tính, bạn biết đấy, từ trái sang phải và từ trên xuống dưới? Hay là người ta buộc phải tạo ra một bức tranh tinh thần về hệ thống dây điện giống như spaghetti trên trang mã được quyết định không tuyến tính? Và nếu người ta phải làm điều đó, chúng ta vẫn phải đọc mã, vậy làm thế nào để chúng ta lấy văn bản tuyến tính và nối dây spaghetti?

Bất kỳ lời khuyên sẽ được đánh giá cao.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25

Câu trả lời:


18

Bạn hầu như gặp khó khăn khi đọc nó bởi vì ví dụ cụ thể này không dễ đọc. Không có ý định xúc phạm, một tỷ lệ lớn các mẫu bạn tìm thấy trên Internet cũng không. Rất nhiều người chỉ chơi xung quanh với lập trình chức năng vào cuối tuần và không bao giờ thực sự phải đối phó với việc duy trì mã chức năng sản xuất lâu dài. Tôi sẽ viết nó như thế này:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

Vì một số lý do, rất nhiều người có ý tưởng này trong đầu rằng mã chức năng nên có một "diện mạo" thẩm mỹ nhất định của một biểu thức lồng nhau lớn. Lưu ý mặc dù phiên bản của tôi hơi giống mã bắt buộc với tất cả các dấu chấm phẩy, mọi thứ đều không thay đổi, vì vậy bạn có thể thay thế tất cả các biến và nhận một biểu thức lớn nếu bạn muốn. Nó thực sự chỉ là "chức năng" như phiên bản spaghetti, nhưng với khả năng đọc dễ dàng hơn.

Ở đây các biểu thức được chia thành các phần rất nhỏ và đặt tên có ý nghĩa cho tên miền. Nesting được tránh bằng cách kéo chức năng phổ biến như mapObjvào một chức năng được đặt tên. Lambdas được dành riêng cho các chức năng rất ngắn với mục đích rõ ràng trong bối cảnh.

Nếu bạn gặp mã khó đọc, hãy cấu trúc lại mã cho đến khi dễ hơn. Nó cần một số thực hành, nhưng nó là giá trị. Mã chức năng có thể dễ đọc như bắt buộc. Trong thực tế, thường là moreso, bởi vì nó thường ngắn gọn hơn.


Chắc chắn không có hành vi phạm tội! Mặc dù tôi vẫn sẽ duy trì rằng tôi biết một số điều về lập trình chức năng, nhưng có thể những khẳng định của tôi trong câu hỏi về mức độ tôi biết là hơi quá mức. Tôi thực sự là một người mới bắt đầu tương đối. Vì vậy, xem làm thế nào nỗ lực đặc biệt này của tôi có thể được viết lại một cách rõ ràng súc tích nhưng vẫn có chức năng như vàng ... cảm ơn bạn. Tôi sẽ nghiên cứu viết lại của bạn một cách cẩn thận.
Andrew Willems

1
Tôi đã nghe nói rằng việc có các chuỗi dài và / hoặc lồng các phương thức sẽ loại bỏ các biến trung gian không cần thiết. Ngược lại, câu trả lời của bạn phá vỡ chuỗi của tôi / lồng vào các câu lệnh độc lập trung gian bằng cách sử dụng các biến trung gian có tên tốt. Tôi thấy mã của bạn dễ đọc hơn trong trường hợp này, nhưng tôi tự hỏi bạn đang cố gắng nói chung như thế nào. Bạn có nói rằng các chuỗi phương thức dài và / hoặc lồng sâu thường hay thậm chí luôn luôn là một kiểu chống phải tránh, hoặc có những lúc chúng mang lại lợi ích đáng kể? Và câu trả lời cho câu hỏi đó có khác nhau đối với mã hóa chức năng so với mệnh lệnh không?
Andrew Willems

3
Có một số tình huống trong đó loại bỏ các biến trung gian có thể thêm rõ ràng. Ví dụ, trong FP bạn hầu như không bao giờ muốn một chỉ mục thành một mảng. Ngoài ra đôi khi không có một tên tuyệt vời cho kết quả trung gian. Tuy nhiên, theo kinh nghiệm của tôi, hầu hết mọi người có xu hướng sai lầm quá xa theo cách khác.
Karl Bielefeldt

6

Tôi đã không thực hiện nhiều công việc có chức năng cao trong Javascript (mà tôi sẽ nói là thế này - hầu hết mọi người nói về Javascript chức năng có thể đang sử dụng bản đồ, bộ lọc và thu nhỏ, nhưng mã của bạn xác định các hàm cấp cao hơn của chính nó , đó là cao hơn một chút so với điều đó), nhưng tôi đã làm như vậy trong Haskell, và tôi nghĩ ít nhất một số kinh nghiệm dịch. Tôi sẽ cung cấp cho bạn một vài gợi ý cho những điều tôi đã học:

Chỉ định các loại chức năng là thực sự quan trọng. Haskell không yêu cầu bạn chỉ định loại hàm là gì, nhưng bao gồm loại trong định nghĩa giúp dễ đọc hơn nhiều. Mặc dù Javascript không hỗ trợ nhập rõ ràng theo cùng một cách, không có lý do gì để không đưa định nghĩa loại vào một nhận xét, ví dụ:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Với một chút thực hành khi làm việc với các định nghĩa kiểu như thế này, chúng làm cho ý nghĩa của hàm rõ ràng hơn nhiều.

Đặt tên rất quan trọng, có lẽ còn hơn cả lập trình thủ tục. Rất nhiều chương trình chức năng được viết theo phong cách rất ngắn gọn, nặng về quy ước (ví dụ: quy ước 'xs' là một danh sách / mảng và 'x' là một mục trong đó rất phổ biến), nhưng trừ khi bạn hiểu phong cách đó dễ dàng tôi đề nghị đặt tên dài dòng hơn. Nhìn vào các tên cụ thể bạn đã sử dụng, "getX" là loại không rõ ràng và do đó "getXs" cũng không thực sự giúp ích nhiều. Tôi sẽ gọi "getXs" một cái gì đó như "applicationToProperIES" và "getX" có thể là "propertyMapper". "getPctSameXs" sau đó sẽ là "PercProperIESSameWith" ("với").

Một điều quan trọng khác là viết mã thành ngữ . Tôi nhận thấy rằng bạn đang sử dụng một cú pháp a => b => some-expression-involving-a-and-bđể tạo ra các hàm bị cong. Điều này rất thú vị và có thể hữu ích trong một số trường hợp, nhưng bạn không làm gì ở đây có lợi từ các hàm bị quấy rối và thay vào đó sẽ sử dụng Javascript thành ngữ để sử dụng nhiều hàm đối số truyền thống. Làm như vậy có thể giúp dễ dàng nhìn thấy những gì đang diễn ra trong nháy mắt. Bạn cũng đang sử dụng const name = lambda-expressionđể xác định các hàm, thay vào đó, nó sẽ là thành ngữ hơn để sử dụng function name (args) { ... }. Tôi biết chúng có chút khác biệt về mặt ngữ nghĩa, nhưng trừ khi bạn dựa vào những khác biệt đó, tôi khuyên bạn nên sử dụng biến thể phổ biến hơn khi có thể.


5
+1 cho các loại! Chỉ vì ngôn ngữ không có chúng, không có nghĩa là bạn không phải nghĩ về chúng . Một số hệ thống tài liệu cho ECMAScript có ngôn ngữ loại để ghi lại các loại chức năng. Một số IDE ECMAScript cũng có ngôn ngữ loại (và thông thường, chúng cũng hiểu ngôn ngữ loại cho các hệ thống tài liệu chính) và thậm chí chúng có thể thực hiện kiểm tra loại thô sơ và gợi ý heuristic bằng cách sử dụng các chú thích loại đó .
Jörg W Mittag

Bạn đã cho tôi rất nhiều thứ để nhai: định nghĩa kiểu, tên có ý nghĩa, sử dụng thành ngữ ... cảm ơn bạn! Chỉ cần một vài trong số nhiều ý kiến ​​có thể: Tôi không nhất thiết có ý định viết một số phần nhất định là các chức năng bị quấy rối; họ chỉ phát triển theo cách đó khi tôi tái cấu trúc mã của mình trong khi viết. Bây giờ tôi có thể thấy nó không cần thiết như thế nào và thậm chí chỉ cần hợp nhất các tham số từ hai hàm đó thành hai tham số cho một hàm duy nhất không chỉ có ý nghĩa hơn mà ngay lập tức làm cho bit ngắn đó ít nhất dễ đọc hơn.
Andrew Willems

@ JörgWMittag, cảm ơn bạn đã nhận xét về tầm quan trọng của các loại và liên kết đến câu trả lời khác mà bạn đã viết. Tôi sử dụng WebStorm và không nhận ra rằng, theo cách tôi đọc câu trả lời khác của bạn, WebStorm biết cách diễn giải các chú thích giống như jsdoc. Tôi giả sử từ nhận xét của bạn rằng jsdoc và WebStorm có thể được sử dụng cùng nhau để chú thích chức năng, không chỉ bắt buộc, mã, mà tôi phải đi sâu hơn để thực sự biết điều đó. Tôi đã chơi với jsdoc trước đây và bây giờ tôi biết rằng WebStorm và tôi có thể hợp tác ở đó, tôi hy vọng tôi sẽ sử dụng tính năng / cách tiếp cận đó nhiều hơn.
Andrew Willems

@Jules, chỉ để làm rõ chức năng bị quấy rối mà tôi đã đề cập trong nhận xét của tôi ở trên: Như bạn ngụ ý, mỗi trường hợp obj => key => ...có thể được đơn giản hóa (obj, key) => ...vì sau này getX(obj)(key)cũng có thể được đơn giản hóa get(obj, key). Ngược lại, một hàm curried khác (getX, filter = vals => vals) => (objA, objB) => ..., không thể dễ dàng đơn giản hóa, ít nhất là trong bối cảnh của phần còn lại của mã như được viết.
Andrew Willems
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.