Tại sao loại suy luận hữu ích?


37

Tôi đọc mã theo cách thường xuyên hơn tôi viết mã và tôi cho rằng hầu hết các lập trình viên làm việc trên phần mềm công nghiệp đều làm việc này. Ưu điểm của suy luận kiểu tôi giả sử là ít chi tiết và ít viết mã hơn. Nhưng mặt khác, nếu bạn đọc mã thường xuyên hơn, có thể bạn sẽ muốn mã có thể đọc được.

Trình biên dịch nhập kiểu; Có những thuật toán cũ cho việc này. Nhưng câu hỏi thực sự là tại sao tôi, lập trình viên, muốn suy ra loại biến của tôi khi tôi đọc mã? Không phải mọi người chỉ đọc loại đó nhanh hơn là nghĩ loại nào ở đó sao?

Chỉnh sửa: Như một kết luận tôi hiểu tại sao nó hữu ích. Nhưng trong danh mục các tính năng ngôn ngữ, tôi thấy nó trong một thùng có quá tải toán tử - hữu ích trong một số trường hợp nhưng ảnh hưởng đến khả năng đọc nếu bị lạm dụng.


5
Theo kinh nghiệm của tôi, các loại rất quan trọng khi viết mã hơn là đọc nó. Khi đọc mã, tôi đang tìm kiếm các thuật toán và các khối cụ thể mà các biến được đặt tên tốt thường sẽ hướng tôi đến. Tôi thực sự không cần phải gõ mã kiểm tra chỉ để đọc và hiểu những gì nó đang làm trừ khi nó được viết rất kém. Tuy nhiên, khi đọc mã có nhiều chi tiết không cần thiết mà tôi không tìm kiếm (như quá nhiều chú thích loại), điều đó khiến cho việc tìm kiếm các bit tôi đang tìm kiếm thường khó khăn hơn. Kiểu suy luận tôi sẽ nói là một lợi ích to lớn cho việc đọc nhiều hơn nhiều so với viết mã.
Jimmy Hoffa

Khi tôi tìm thấy đoạn mã tôi đang tìm, sau đó tôi có thể bắt đầu kiểm tra mã nhưng tại bất kỳ thời điểm nào, bạn không nên tập trung vào hơn 10 dòng mã, tại thời điểm đó không có gì khó khăn để thực hiện suy luận về bản thân vì bạn bắt đầu chọn toàn bộ khối để bắt đầu và có thể sử dụng công cụ để giúp bạn làm điều đó bằng mọi cách. Tìm ra các loại trong 10 dòng mã mà bạn đang cố gắng sử dụng hiếm khi mất nhiều thời gian của bạn, nhưng đây là phần mà bạn đã chuyển từ đọc sang viết dù sao nó ít phổ biến hơn.
Jimmy Hoffa

Hãy nhớ rằng mặc dù một lập trình viên đọc mã thường xuyên hơn viết nó, điều đó không có nghĩa là một đoạn mã được đọc thường xuyên hơn viết. Rất nhiều mã có thể tồn tại trong thời gian ngắn hoặc không bao giờ đọc lại, và có thể không dễ để biết mã nào sẽ tồn tại và nên được viết để dễ đọc tối đa.
jpa

2
Xây dựng quan điểm đầu tiên của @JimmyHoffa, xem xét việc đọc nói chung. Là một câu dễ dàng hơn để phân tích và đọc, hãy để một mình hiểu, khi tập trung vào phần nói của các từ riêng lẻ của nó? "Con bò (bài báo) bò (danh từ số ít) nhảy (động từ quá khứ) qua (giới từ) mặt trăng (bài viết) (danh từ). (Dấu chấm câu)".
Zev Spitz

Câu trả lời:


46

Hãy xem Java. Java không thể có các biến với các kiểu suy ra. Điều này có nghĩa là tôi thường xuyên phải đánh vần loại, ngay cả khi nó hoàn toàn rõ ràng đối với người đọc con người loại đó là gì:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

Và đôi khi thật khó chịu khi đánh vần toàn bộ loại.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Kiểu gõ tĩnh dài này cản trở tôi, lập trình viên. Hầu hết các chú thích kiểu là dòng điền lặp đi lặp lại, hồi quy không có nội dung về những gì chúng ta đã biết. Tuy nhiên, tôi thích gõ tĩnh, vì nó thực sự có thể giúp phát hiện ra các lỗi, vì vậy sử dụng gõ động không phải lúc nào cũng là một câu trả lời hay. Kiểu suy luận là tốt nhất của cả hai thế giới: Tôi có thể bỏ qua các loại không liên quan, nhưng vẫn chắc chắn rằng chương trình của tôi (loại-) kiểm tra.

Mặc dù suy luận kiểu thực sự hữu ích cho các biến cục bộ, nhưng nó không nên được sử dụng cho các API công khai phải được ghi lại rõ ràng. Và đôi khi các loại thực sự quan trọng để hiểu những gì đang diễn ra trong mã. Trong những trường hợp như vậy, sẽ thật ngu ngốc nếu chỉ dựa vào suy luận kiểu.

Có nhiều ngôn ngữ hỗ trợ suy luận kiểu. Ví dụ:

  • C ++. Các autotừ khóa kích hoạt suy luận loại. Không có nó, đánh vần các loại cho lambdas hoặc cho các mục trong container sẽ là địa ngục.

  • C #. Bạn có thể khai báo các biến với var, điều này kích hoạt một dạng suy luận kiểu giới hạn. Nó vẫn quản lý hầu hết các trường hợp mà bạn muốn suy luận kiểu. Ở một số nơi nhất định, bạn có thể loại bỏ hoàn toàn loại (ví dụ như trong lambdas).

  • Haskell, và bất kỳ ngôn ngữ nào trong gia đình ML. Mặc dù hương vị cụ thể của suy luận loại được sử dụng ở đây khá mạnh mẽ, bạn vẫn thường thấy các chú thích loại cho các hàm và vì hai lý do: Thứ nhất là tài liệu và thứ hai là kiểm tra loại suy luận thực sự tìm thấy các loại bạn mong đợi. Nếu có sự khác biệt, có khả năng có một số loại lỗi.


13
Cũng lưu ý rằng C # có các loại ẩn danh, tức là các loại không có tên, nhưng C # có một hệ thống loại danh nghĩa, tức là một hệ thống loại dựa trên tên. Không có suy luận kiểu, những loại đó không bao giờ có thể được sử dụng!
Jörg W Mittag

10
Một số ví dụ là một chút giả định, theo ý kiến ​​của tôi. Khởi tạo thành 42 không tự động có nghĩa là biến là một int, nó có thể là bất kỳ loại số nào kể cả chẵn char. Ngoài ra tôi không thấy lý do tại sao bạn muốn đánh vần toàn bộ loại cho Entrykhi bạn chỉ cần nhập tên lớp và để IDE của bạn thực hiện nhập cần thiết. Trường hợp duy nhất khi bạn phải đánh vần toàn bộ tên là khi bạn có một lớp có cùng tên trong gói của riêng bạn. Nhưng có vẻ như tôi thích thiết kế tồi.
Malcolm

10
@Malcolm Vâng, tất cả các ví dụ của tôi đều bị chiếm đoạt. Họ phục vụ để minh họa một điểm. Khi tôi viết intví dụ, tôi đã suy nghĩ về (theo ý kiến ​​của tôi về hành vi khá lành mạnh) của hầu hết các ngôn ngữ có tính năng suy luận kiểu. Họ thường suy luận một inthoặc Integerbất cứ điều gì nó được gọi bằng ngôn ngữ đó. Vẻ đẹp của suy luận kiểu là nó luôn luôn là tùy chọn; bạn vẫn có thể chỉ định một loại khác nếu bạn cần. Về Entryví dụ: điểm tốt, tôi sẽ thay thế nó bằng Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java thậm chí không có bí danh loại :(
amon

4
@ m3th0dman Nếu loại quan trọng để hiểu, thì bạn vẫn có thể đề cập rõ ràng về nó. Kiểu suy luận luôn luôn là tùy chọn. Nhưng ở đây, loại colKeyvừa rõ ràng vừa không liên quan: chúng tôi chỉ quan tâm đến việc nó có phù hợp như là đối số thứ hai của doSomethingWith. Nếu tôi trích xuất vòng lặp đó thành một hàm mang lại Iterable của (key1, key2, value)-triples, chữ ký chung nhất sẽ là <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Bên trong chức năng đó, loại thực sự colKey( Integer, không K2) hoàn toàn không liên quan.
amon

4
@ m3th0dman đó là một tuyên bố khá rộng rãi, về mã " hầu hết " là cái này hay cái kia. Thống kê giai thoại. Chắc chắn không có điểm nào trong việc viết loại hai lần trong khởi tạo : View.OnClickListener listener = new View.OnClickListener(). Bạn vẫn sẽ biết loại ngay cả khi lập trình viên "lười biếng" và rút ngắn nó xuống var listener = new View.OnClickListener(nếu điều này là có thể). Loại dư thừa này là phổ biến - tôi sẽ không mạo hiểm dự đoán ở đây - và loại bỏ nó không xuất phát từ suy nghĩ về độc giả tương lai. Mọi tính năng ngôn ngữ nên được sử dụng cẩn thận, tôi không nghi ngờ điều đó.
Konrad Morawski

26

Đúng là mã được đọc thường xuyên hơn nhiều so với mã được viết. Tuy nhiên, việc đọc cũng mất thời gian và hai màn hình mã khó điều hướng và đọc hơn một màn hình mã, vì vậy chúng tôi cần ưu tiên để đóng gói tỷ lệ thông tin / nỗ lực đọc thông tin hữu ích tốt nhất. Đây là một nguyên tắc chung của UX: Quá nhiều thông tin cùng một lúc áp đảo và thực sự làm giảm hiệu quả của giao diện.

Và theo kinh nghiệm của tôi, thông thường , loại chính xác không quan trọng. Chắc chắn bạn đôi khi tổ biểu thức: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Mỗi trong số này chứa các biểu thức con đánh giá một giá trị mà loại không được viết ra. Tuy nhiên, họ là hoàn toàn rõ ràng. Chúng tôi sẽ ổn khi để loại không được nêu ra vì nó đủ dễ để tìm ra từ ngữ cảnh và viết nó ra sẽ gây hại nhiều hơn là tốt (làm xáo trộn sự hiểu biết về luồng dữ liệu, chiếm màn hình có giá trị và không gian bộ nhớ ngắn hạn).

Một lý do để không lồng các biểu thức như không có ngày mai là các dòng dài và dòng giá trị trở nên không rõ ràng. Giới thiệu một biến tạm thời giúp với điều này, nó áp đặt một thứ tự và đặt tên cho một kết quả một phần. Tuy nhiên, không phải tất cả mọi thứ được hưởng lợi từ các khía cạnh này cũng được hưởng lợi từ việc loại của nó được viết ra:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Liệu nó có quan trọng cho dù userlà một đối tượng thực thể, một số nguyên, một chuỗi, hoặc một cái gì khác? Đối với hầu hết các mục đích, không đủ để biết rằng nó đại diện cho người dùng, xuất phát từ yêu cầu HTTP và được sử dụng để tìm nạp tên để hiển thị ở góc dưới bên phải của câu trả lời.

Và khi nó quan trọng, tác giả có thể tự do viết ra loại. Đây là một quyền tự do phải được sử dụng có trách nhiệm, nhưng điều này cũng đúng với mọi thứ khác có thể tăng cường khả năng đọc (tên biến và hàm, định dạng, thiết kế API, khoảng trắng). Và thực tế, quy ước trong Haskell và ML (nơi mọi thứ có thể được suy ra mà không cần nỗ lực thêm) là viết ra các loại hàm chức năng không cục bộ, và cả các biến và hàm cục bộ bất cứ khi nào thích hợp. Chỉ người mới để mọi loại được suy luận.


2
+1 Đây phải là câu trả lời được chấp nhận. Nó đi thẳng vào trọng tâm của lý do tại sao suy luận kiểu là một ý tưởng tuyệt vời.
Christian Hayter

Loại chính xác của uservấn đề nếu bạn đang cố gắng mở rộng chức năng, bởi vì nó xác định những gì bạn có thể làm với user. Điều này rất quan trọng nếu bạn muốn thêm một số kiểm tra độ tỉnh táo (ví dụ do lỗ hổng bảo mật) hoặc quên rằng bạn thực sự cần phải làm gì đó với người dùng ngoài việc chỉ hiển thị nó. Đúng, những kiểu đọc để mở rộng ít thường xuyên hơn là chỉ đọc mã, nhưng chúng cũng là một phần quan trọng trong công việc của chúng tôi.
cmaster

@cmaster Và bạn luôn có thể tìm ra loại đó khá dễ dàng (hầu hết các IDE sẽ cho bạn biết, và có giải pháp công nghệ thấp cố tình gây ra lỗi loại và để trình biên dịch in loại thực tế), nó hoàn toàn không phù hợp không làm phiền bạn trong trường hợp phổ biến.

4

Tôi nghĩ rằng suy luận kiểu là khá quan trọng và nên được hỗ trợ trong bất kỳ ngôn ngữ hiện đại nào. Tất cả chúng ta đều phát triển trong IDE và chúng có thể giúp ích rất nhiều trong trường hợp bạn muốn biết loại suy ra, chỉ một vài trong số chúng ta hack vi. Hãy nghĩ về tính dài dòng và mã lễ trong Java chẳng hạn.

  Map<String,HashMap<String,String>> map = getMap();

Nhưng bạn có thể nói nó ổn, IDE của tôi sẽ giúp tôi, nó có thể là một điểm hợp lệ. Tuy nhiên, một số tính năng sẽ không có ở đó mà không có sự trợ giúp của kiểu suy luận, ví dụ như các loại ẩn danh C #.

 var person = new {Name="John Smith", Age = 105};

LINQ sẽ không được như thoải mái như bây giờ mà không cần sự giúp đỡ của suy luận kiểu, Selectví dụ

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Loại ẩn danh này sẽ được suy luận gọn gàng với biến.

Tôi không thích kiểu suy luận về kiểu trả về Scalavì tôi nghĩ rằng điểm của bạn áp dụng ở đây, nên rõ ràng cho chúng ta biết hàm nào trả về để chúng ta có thể sử dụng API trôi chảy hơn


Map<String,HashMap<String,String>>? Chắc chắn, nếu bạn không sử dụng các loại, thì việc đánh vần chúng có ít lợi ích. Table<User, File, String>là thông tin nhiều hơn mặc dù, và có lợi ích trong việc viết nó.
MikeFHay

4

Tôi nghĩ rằng câu trả lời cho điều này thực sự đơn giản: nó tiết kiệm việc đọc và viết thông tin dư thừa. Đặc biệt trong các ngôn ngữ hướng đối tượng, nơi bạn có một loại ở cả hai bên của dấu bằng.

Điều này cũng cho bạn biết khi nào bạn nên hoặc không nên sử dụng nó - khi thông tin không dư thừa.


3
Về mặt kỹ thuật, thông tin luôn dư thừa khi có thể bỏ qua các chữ ký thủ công: nếu không trình biên dịch sẽ không thể suy ra chúng! Nhưng tôi hiểu ý của bạn: khi bạn chỉ sao chép một chữ ký thành nhiều điểm trong một chế độ xem, nó thực sự dư thừa cho bộ não , trong khi một số loại được đặt tốt cung cấp thông tin bạn phải tìm kiếm trong một thời gian dài, có thể với một tốt nhiều biến đổi không biểu hiện.
leftaroundabout

@leftaroundabout: dự phòng khi đọc bởi lập trình viên.
jmoreno

3

Giả sử người ta nhìn thấy mã:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Nếu someBigLongGenericTypeđược gán từ loại trả về someFactoryMethod, thì khả năng ai đó đọc mã sẽ chú ý nếu các loại không khớp chính xác và người nào có thể nhận thấy sự khác biệt dễ dàng nhận ra liệu đó có phải là cố ý hay không?

Bằng cách cho phép suy luận, một ngôn ngữ có thể gợi ý cho ai đó đang đọc mã rằng khi loại biến được nêu rõ ràng, người đó nên cố gắng tìm một lý do cho nó. Điều này lần lượt cho phép những người đang đọc mã tập trung tốt hơn các nỗ lực của họ. Ngược lại, nếu phần lớn các lần khi một loại được chỉ định, nó sẽ giống hệt như những gì đã được suy ra, thì ai đó đang đọc mã có thể ít chú ý đến những lần nó khác biệt .


2

Tôi thấy rằng có một số câu trả lời tốt. Một số trong đó tôi sẽ nhắc lại nhưng đôi khi bạn chỉ muốn đặt mọi thứ vào từ của riêng bạn. Tôi sẽ bình luận với một số ví dụ từ C ++ vì đó là ngôn ngữ mà tôi quen thuộc nhất.

Điều cần thiết là không bao giờ không khôn ngoan. Kiểu suy luận là cần thiết để làm cho các tính năng ngôn ngữ khác thực tế. Trong C ++ có thể có các loại không thể nhầm lẫn.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11 thêm lambdas mà cũng không thể lộn xộn.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

Gõ suy luận cũng là nền tảng cho các mẫu.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Nhưng câu hỏi của bạn là "tại sao tôi, lập trình viên, muốn suy ra loại biến của tôi khi tôi đọc mã? Có phải mọi người chỉ đọc loại đó nhanh hơn là nghĩ loại nào không?"

Kiểu suy luận loại bỏ sự dư thừa. Khi nói đến việc đọc mã đôi khi có thể nhanh hơn và dễ dàng hơn để có thông tin dư thừa trong mã nhưng sự dư thừa có thể làm lu mờ thông tin hữu ích . Ví dụ:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Không có nhiều sự quen thuộc với thư viện chuẩn cho một lập trình viên C ++ để xác định rằng tôi là một trình vòng lặp từ i = v.begin()đó khai báo kiểu rõ ràng có giá trị giới hạn. Bằng sự hiện diện của nó, nó che khuất các chi tiết quan trọng hơn (chẳng hạn như chỉ ra iđiểm bắt đầu của vectơ). Câu trả lời hay của @amon cung cấp một ví dụ thậm chí tốt hơn về tính dài dòng làm lu mờ các chi tiết quan trọng. Ngược lại, sử dụng suy luận kiểu sẽ làm nổi bật hơn các chi tiết quan trọng.

std::vector<int> v;
auto i = v.begin();

Mặc dù đọc mã là quan trọng nhưng nó không đủ, đến một lúc nào đó bạn sẽ phải ngừng đọc và bắt đầu viết mã mới. Sự dư thừa trong mã làm cho việc sửa đổi mã chậm hơn và khó hơn. Ví dụ: giả sử tôi có đoạn mã sau:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Trong trường hợp tôi cần thay đổi loại giá trị của vectơ để thay đổi mã thành:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

Trong trường hợp này tôi phải sửa đổi mã ở hai nơi. Tương phản với suy luận kiểu trong đó mã gốc là:

std::vector<int> v;
auto i = v.begin();

Và mã sửa đổi:

std::vector<double> v;
auto i = v.begin();

Lưu ý rằng bây giờ tôi chỉ phải thay đổi một dòng mã. Ngoại suy điều này thành một chương trình lớn và suy luận kiểu có thể truyền các thay đổi thành các loại nhanh hơn nhiều so với bạn có thể với một trình soạn thảo.

Sự dư thừa trong mã tạo ra khả năng lỗi. Bất cứ lúc nào mã của bạn phụ thuộc vào hai mẩu thông tin được giữ tương đương có khả năng xảy ra lỗi. Ví dụ, có một sự không nhất quán giữa hai loại trong tuyên bố này có lẽ không có ý định:

int pi = 3.14159;

Sự dư thừa làm cho ý định khó phân biệt hơn. Trong một số trường hợp, suy luận kiểu có thể dễ đọc và dễ hiểu hơn vì nó đơn giản hơn so với đặc tả kiểu rõ ràng. Hãy xem xét đoạn mã:

int y = sq(x);

Trong trường hợp sq(x)trả về một int, không rõ yintvì nó là kiểu trả về sq(x)hay vì nó phù hợp với các câu lệnh sử dụng y. Nếu tôi thay đổi mã khác sao cho sq(x)không còn trả về int, thì không chắc chắn từ dòng đó có phải là loại ycập nhật hay không. Tương phản với cùng một mã nhưng sử dụng kiểu suy luận:

auto y = sq(x);

Trong đó ý định là rõ ràng, yphải cùng loại với trả về sq(x). Khi mã thay đổi loại trả về sq(x), loại ythay đổi sẽ tự động khớp.

Trong C ++ có một lý do thứ hai tại sao ví dụ trên đơn giản hơn với suy luận kiểu, suy luận kiểu không thể đưa ra chuyển đổi kiểu ẩn. Nếu kiểu trả về sq(x)là không int, trình biên dịch sẽ âm thầm chèn một chuyển đổi ngầm định sang int. Nếu kiểu trả về sq(x)là loại phức tạp xác định operator int(), lệnh gọi hàm ẩn này có thể phức tạp tùy ý.


Khá là một điểm tốt về các loại không thể nhầm lẫn trong C ++. Tuy nhiên, tôi nghĩ đó không phải là lý do để thêm suy luận kiểu, hơn là lý do để sửa ngôn ngữ. Trong trường hợp đầu tiên bạn trình bày, lập trình viên chỉ cần đặt tên cho nó để tránh sử dụng suy luận kiểu, vì vậy đây không phải là một ví dụ mạnh. Ví dụ thứ hai chỉ mạnh vì C ++ rõ ràng cấm các loại lambda là không thể nói được, thậm chí việc đánh máy bằng cách sử dụng typeofcũng bị ngôn ngữ vô dụng. Và đó là một sự thiếu hụt của chính ngôn ngữ nên được sửa chữa theo ý kiến ​​của tôi.
cmaster
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.