Ubuntu TechHive
rust-and-data-processing-with-polars.md
रस्ट और पोलर्स के साथ डेटा प्रोसेसिंग
article.विवरण

रस्ट और पोलर्स के साथ डेटा प्रोसेसिंग

reading.प्रगति 14 मिनट पढ़ें

Polars का उपयोग करके डेटा प्रोसेसिंग के साथ-साथ Rust की बुनियादी बातों का एक संक्षिप्त परिचय

Polars के साथ Rust डेटा प्रोसेसिंग

Rust को क्या अलग बनाता है

  • कंपाइल और तेज़ — नेटिव मशीन कोड में कंपाइल होता है, कोई रनटाइम/GC नहीं
  • मेमोरी सुरक्षित — कंपाइलर आपके प्रोग्राम के चलने से पहले ही बग्स की पूरी श्रेणियों (नल पॉइंटर एरर, डेटा रेस) को रोक देता है
  • मजबूत, स्टेटिकली टाइप्ड — हर वैल्यू का एक टाइप होता है जो कंपाइल समय पर ज्ञात होता है; कंपाइलर बेमेल (mismatches) को जल्दी पकड़ लेता है

वेरिएबल और म्यूटेबिलिटी (परिवर्तनशीलता)

वेरिएबल डिफ़ॉल्ट रूप से अपरिवर्तनीय (immutable) होते हैं। आप mut के साथ म्यूटेबिलिटी चुनते हैं।

let x = 5;          // अपरिवर्तनीय -- दोबारा असाइन नहीं किया जा सकता
let mut y = 10;     // परिवर्तनीय
y = 20;             // `mut` के कारण ठीक है
// x = 6;           // कंपाइल एरर: `x` को दो बार असाइन नहीं किया जा सकता

const MAX: u32 = 100_000;  // स्थिरांक (constant): हमेशा अपरिवर्तनीय, टाइप आवश्यक है

यह डिफ़ॉल्ट सामान्य अपेक्षा को उलट देता है: आप पहले ही बता देते हैं कि क्या बदलने की अनुमति है, जिससे कोड के बारे में तर्क करना आसान हो जाता है।

बुनियादी डेटा प्रकार

स्केलर प्रकार

  • पूर्णांक (Integers): i32, i64, u32, u64 … (i = साइन्ड, u = अनसाइन्ड; नंबर = बिट्स)। i32 डिफ़ॉल्ट है।
  • फ्लोट्स: f64 (डिफ़ॉल्ट), f32
  • बूलियन: bool -> true / false
  • कैरेक्टर: char -> एक एकल यूनिकोड कैरेक्टर, सिंगल कोट्स में
let count: i64 = 42;
let price: f64 = 19.99;
let is_ready: bool = true;
let letter: char = 'A';

कंपाउंड प्रकार

  • टुपल (Tuple): मिश्रित प्रकारों का निश्चित आकार का समूह
  • ऐरे (Array): निश्चित आकार, सभी एक ही प्रकार के
let person: (i32, f64, char) = (30, 5.9, 'M');
let height = person.1;          // इंडेक्स द्वारा एक्सेस -> 5.9

let nums: [i32; 3] = [1, 2, 3]; // 3 i32s की ऐरे
let first = nums[0];            // -> 1

स्ट्रिंग्स: दो प्रकार

  • &str — एक "स्ट्रिंग स्लाइस", आमतौर पर एक निश्चित/उधार लिया गया स्ट्रिंग लिटरल
  • String — एक ओन्ड (owned), बढ़ने योग्य स्ट्रिंग जिसे आप संशोधित कर सकते हैं
let literal: &str = "hello";          // निश्चित टेक्स्ट
let mut owned: String = String::from("hello");
owned.push_str(", world");            // बढ़ सकता है क्योंकि यह ओन्ड है

फंक्शन्स

  • fn के साथ घोषित किए जाते हैं
  • पैरामीटर प्रकार आवश्यक हैं; रिटर्न टाइप -> के बाद आता है
  • अंतिम एक्सप्रेशन (बिना सेमीकोलन के) रिटर्न वैल्यू होती है
fn add(a: i32, b: i32) -> i32 {
    a + b          // कोई सेमीकोलन नहीं = यह रिटर्न वैल्यू है
}

fn greet(name: &str) {   // कोई `->` नहीं का मतलब है कि यह कुछ भी रिटर्न नहीं करता
    println!("Hello, {name}!");
}

fn main() {
    let sum = add(2, 3);     // हर प्रोग्राम main() से शुरू होता है
    println!("Sum: {sum}");
    greet("Aziz");
}

नोट: println! एक मैक्रो है (! इसे दर्शाता है), फंक्शन नहीं।

कंट्रोल फ्लो

if / else (यह एक एक्सप्रेशन है!)

let n = 7;
if n % 2 == 0 {
    println!("even");
} else {
    println!("odd");
}

// क्योंकि `if` एक वैल्यू रिटर्न करता है, आप इसके साथ असाइन कर सकते हैं:
let label = if n > 5 { "big" } else { "small" };

लूप्स

// loop: जब तक आप `break` न करें, हमेशा चलता रहता है
let mut i = 0;
loop {
    if i >= 3 { break; }
    i += 1;
}

// while
let mut c = 3;
while c > 0 {
    println!("{c}");
    c -= 1;
}

// for: सबसे सामान्य -- रेंज या कलेक्शन पर इटरेट करें
for k in 0..3 {            // 0, 1, 2  (अंत-अनन्य)
    println!("k = {k}");
}

ओनरशिप (Ownership): मुख्य विचार

Rust की मुख्य विशेषता। तीन नियम:

  1. प्रत्येक वैल्यू का एक ओनर होता है
  2. एक समय में केवल एक ही ओनर हो सकता है
  3. जब ओनर स्कोप से बाहर जाता है, तो वैल्यू को साफ कर दिया जाता है
let s1 = String::from("hi");
let s2 = s1;              // ओनरशिप s2 में चली जाती है (MOVES)
// println!("{s1}");      // एरर: s1 अब मान्य नहीं है

// किसी अन्य फंक्शन को ओनरशिप लिए बिना वैल्यू का उपयोग करने देने के लिए,
// आप इसे & (एक रेफरेंस) के साथ *उधार (borrow)* लेते हैं:
fn length(s: &String) -> usize {
    s.len()              // s को पढ़ता है, इसका ओनर नहीं है
}
let word = String::from("rust");
let n = length(&word);   // इसे उधार दें; `word` बाद में भी उपयोग करने योग्य है

यही वह चीज़ है जो Rust को गारबेज कलेक्टर के बिना मेमोरी सुरक्षा की गारंटी देने देती है। यह वह हिस्सा है जिसे समझने में सबसे अधिक समय लगता है।

स्ट्रक्ट्स (Structs): कस्टम डेटा प्रकार

struct Order  {
    id: i64,
    amount: f64,
    shipped: bool,
}

let o = Order { id: 1, amount: 42.5, shipped: true };
println!("Order {} costs {}", o.id, o.amount);

एनम्स (Enums) और पैटर्न मैचिंग

एनम्स एक वैल्यू को कई वेरिएंट्स में से एक होने देते हैं; match प्रत्येक को संभालता है।

enum Status {
    Pending,
    Shipped,
    Cancelled,
}

let s = Status::Shipped;
match s {
    Status::Pending   => println!("waiting"),
    Status::Shipped   => println!("on the way"),
    Status::Cancelled => println!("nope"),
}

match पूर्ण (exhaustive) होना चाहिए — हर मामले को संभालें अन्यथा कोड कंपाइल नहीं होगा। यह एक और तरीका है जिससे कंपाइलर आपको चीजें भूलने से रोकता है।

Option और Result: कोई नल नहीं, कोई साइलेंट एरर नहीं

Rust में null नहीं होता। इसके बजाय:

  • Option — एक वैल्यू जो या तो Some(x) है या None
  • Result — या तो Ok(x) है या Err(e) (यह Polars उदाहरणों में सभी एरर हैंडलिंग का आधार है)
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

match divide(10.0, 2.0) {
    Some(result) => println!("Got {result}"),
    None         => println!("Can't divide by zero"),
}

? ऑपरेटर: एरर हैंडलिंग शॉर्टहैंड

Result पर, ? का मतलब है "मुझे वैल्यू दें, या इस फंक्शन से एरर रिटर्न करें।"

use std::num::ParseIntError;

fn parse_and_double(text: &str) -> Result<i32, ParseIntError> {
    let n = text.parse::<i32>()?;  // यदि पार्स विफल होता है, तो Err रिटर्न करें
    Ok(n * 2)                      // अन्यथा आगे बढ़ें
}

यही कारण है कि read_orders(...)? साफ-सुथरा दिखता है: ? चुपचाप किसी भी विफलता को आगे बढ़ा देता है बजाय इसके कि एक बड़ा मैच ब्लॉक मजबूर करे।

सामान्य कलेक्शन्स

  • Vec — बढ़ने योग्य सूची (पायथन सूची की तरह)
  • HashMap — की/वैल्यू मैप (पायथन डिक्ट की तरह)
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
for item in &v { println!("{item}"); }

use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("alice", 10);
scores.insert("bob", 7);

Cargo: Rust का बिल्ड टूल और पैकेज मैनेजर

आवश्यक चीजें:

cargo new my_project   # नया प्रोजेक्ट बनाएं
cargo build            # कंपाइल करें
cargo run              # कंपाइल + रन करें
cargo test             # टेस्ट चलाएं
cargo add polars       # Cargo.toml में डिपेंडेंसी जोड़ें

डिपेंडेंसी ("crates" कहलाती हैं) Cargo.toml में घोषित की जाती हैं और crates.io से ली जाती हैं।

ध्यान देने योग्य बातें:

  • ओनरशिप / उधार लेना& और mut का नृत्य। शुरुआत में इससे संघर्ष करने की अपेक्षा करें; अभ्यास के साथ यह समझ में आ जाता है।
  • दो स्ट्रिंग प्रकार (String बनाम &str) — .to_string() या String::from(...) के साथ कन्वर्ट करें।
  • डिफ़ॉल्ट रूप से अपरिवर्तनीयmut भूलना सबसे आम शुरुआती गलती है।
  • कंपाइलर आपका मित्र है — Rust के एरर मैसेज असामान्य रूप से अच्छे होते हैं। उन्हें पढ़ें; वे अक्सर आपको सटीक समाधान बताते हैं।
  • मैक्रो बनाम फंक्शन्सprintln!, vec!, df! ! पर समाप्त होते हैं और सामान्य फंक्शन्स से थोड़ा अलग व्यवहार करते हैं।

Polars क्या है

  • सारणीबद्ध डेटा (पंक्तियों और स्तंभों) के साथ काम करने के लिए एक DataFrame लाइब्रेरी — कोड में स्प्रेडशीट या डेटाबेस टेबल के बारे में सोचें
  • Rust में लिखा गया, Apache Arrow (एक कॉलम-आधारित मेमोरी फॉर्मेट) पर निर्मित
  • कॉलम-आधारित (Columnar): डेटा को पंक्ति के बजाय कॉलम द्वारा संग्रहीत करता है — यही कारण है कि कॉलम ऑपरेशन और एनालिटिक्स तेज़ होते हैं
  • डिफ़ॉल्ट रूप से मल्टीथ्रेडेड: आपके पूछे बिना आपके सभी CPU कोर का उपयोग करता है
  • सीधे Rust से, और बाइंडिंग के माध्यम से पायथन से उपलब्ध

दो मुख्य प्रकार

  • Series — डेटा का एक एकल कॉलम, सभी एक ही प्रकार के
  • DataFrame — Series का एक संग्रह; टेबल स्वयं
use polars::prelude::*;

// एक Series एक नामित कॉलम है।
let s = Series::new("amount".into(), &[42.5, 17.0, 9.99]);

// एक DataFrame कॉलम से बनाया जाता है। df! मैक्रो आसान तरीका है।
let df = df!(
    "order_id" => &[1, 2, 3],
    "amount"   => &[42.5, 17.0, 9.99],
)?;
println!("{df}");

नोट df! ! पर समाप्त होता है — यह एक मैक्रो है, जैसे println! और =vec!=।

सब कुछ एक Result रिटर्न करता है

लगभग हर Polars ऑपरेशन विफल हो सकता है (खराब प्रकार, गायब कॉलम, खराब फाइलें), इसलिए यह PolarsResult रिटर्न करता है। इसीलिए आप वर्कशॉप में हर जगह ? देखते हैं — यह एरर को चुपचाप गुजरने देने के बजाय आगे बढ़ाता है।

fn build() -> PolarsResult<DataFrame> {
    let df = df!("a" => &[1, 2, 3])?;   // ? अनरैप करता है या एरर रिटर्न करता है
    Ok(df)
}

यह सीधे Rust के Result और ? से जुड़ता है: खराब डेटा एक ऐसी एरर बन जाता है जिसे आपको संभालना होगा, न कि एक साइलेंट =NaN=।

डेटा पढ़ना और लिखना

एजेंडा से चार फॉर्मेट:

// CSV इन
let df = CsvReadOptions::default()
    .with_has_header(true)
    .try_into_reader_with_file_path(Some("orders.csv".into()))?
    .finish()?;

// Parquet आउट
let mut file = std::fs::File::create("orders.parquet")?;
ParquetWriter::new(&mut file).finish(&mut df)?;

// Parquet इन
let mut f = std::fs::File::open("orders.parquet")?;
let df = ParquetReader::new(&mut f).finish()?;

मुख्य विचार: Parquet फाइल के अंदर स्कीमा और प्रकारों को संग्रहीत करता है, इसलिए इसे वापस पढ़ने के लिए किसी अनुमान की आवश्यकता नहीं होती है। CSV टेक्स्ट है और इसे अनुमानित किया जाना चाहिए या एक स्पष्ट स्कीमा दिया जाना चाहिए।

स्कीमा: अनुबंध (Contract)

एक Schema प्रत्येक कॉलम के नाम और प्रकार को पहले से घोषित करता है। किसी रीडर को एक दें और खराब डेटा कॉलम को दूषित करने के बजाय जोर से विफल हो जाएगा।

let mut schema = Schema::default();
schema.with_column("order_id".into(), DataType::Int64);
schema.with_column("amount".into(), DataType::Float64);

सामान्य DataType=s: =Int64, Float64, String, Boolean, =Date=।

चयन और फ़िल्टरिंग

आप एक्सप्रेशन के साथ ऑपरेशनों का वर्णन करते हैं — col(...) एक कॉलम को संदर्भित करता है, और आप ट्रांसफॉर्मेशन को चेन करते हैं।

let result = df
    .clone()
    .lazy()
    .filter(col("status").eq(lit("shipped")))   // मिलान वाली पंक्तियों को रखें
    .select([col("order_id"), col("amount")])    // कॉलम चुनें
    .collect()?;                                  // इसे चलाएं
  • col("x") — कॉलम x को संदर्भित करें
  • lit("shipped") — तुलना करने के लिए एक लिटरल वैल्यू
  • .eq, .gt, .lt — एक्सप्रेशन पर तुलना ऑपरेटर

जॉइन्स: टेबल को जोड़ना

साझा की पर दो DataFrames से पंक्तियों का मिलान करें।

let joined = orders.join(
    &customers,
    ["customer_id"],                 // बाईं टेबल में की
    ["customer_id"],                 // दाईं टेबल में की
    JoinArgs::new(JoinType::Inner),  // Inner / Left / Anti / ...
    None,
)?;

जानने योग्य जॉइन प्रकार:

  • Inner — केवल वे पंक्तियाँ जो दोनों में मेल खाती हैं
  • Left — सभी बाईं पंक्तियाँ, जहाँ कोई मिलान नहीं है वहाँ नल
  • Antiबिना मिलान वाली बाईं पंक्तियाँ (डेटा-गुणवत्ता जांच के रूप में बढ़िया)

Eager बनाम Lazy: बड़ा अंतर

  • Eager — प्रत्येक ऑपरेशन तुरंत चलता है (DataFrame)। सरल, छोटे डेटा और अन्वेषण के लिए अच्छा।
  • Lazy — आप एक क्वेरी प्लान बनाते हैं, और .collect() तक कुछ भी नहीं चलता है। Polars फिर पूरे प्लान को ऑप्टिमाइज़ करता है (फ़िल्टर को नीचे धकेलना, केवल आवश्यक कॉलम पढ़ना)।
// Lazy: scan_* और .lazy() एक LazyFrame रिटर्न करते हैं -- एक प्लान, अभी तक डेटा नहीं।
let plan = LazyCsvReader::new(PlPath::new("orders.csv"))
    .with_has_header(true)
    .finish()?
    .filter(col("status").eq(lit("shipped")))
    .select([col("order_id"), col("amount")]);

println!("{}", plan.clone().explain(true)?);  // प्लान का निरीक्षण करें
let df = plan.collect()?;                       // अब यह चलता है

explain(true) ऑप्टिमाइज़्ड प्लान को प्रिंट करता है — आप देख सकते हैं कि इंजन ने कोई कंप्यूट खर्च करने से पहले क्या करने का निर्णय लिया।

सामान्य ऑपरेशनों की चीट शीट

df.height();                 // पंक्तियों की संख्या
df.width();                  // कॉलम की संख्या
df.column("amount")?;        // एक कॉलम (Series) प्राप्त करें
df.head(Some(5));            // पहली 5 पंक्तियाँ
df.get_column_names();       // कॉलम के नाम
df.column("amount")?.dtype();// कॉलम का डेटा प्रकार

Polars क्यों (pandas / Spark बनाम)

  • pandas बनाम — बहुत तेज़, मल्टीथ्रेडेड, लेज़ी ऑप्टिमाइज़ेशन, बेहतर मेमोरी व्यवहार; प्रकार सख्त हैं (कम साइलेंट आश्चर्य)
  • Spark बनाम — सिंगल-मशीन वर्कलोड के लिए किसी क्लस्टर की आवश्यकता नहीं; कई "हमें Spark की आवश्यकता है" जॉब्स वास्तव में "pandas एक बॉक्स पर बहुत धीमा था" होते हैं
  • Polars आपको वितरित-सिस्टम ओवरहेड के बिना प्रदर्शन और शुद्धता देता है

यह Rust बेसिक्स से कैसे जुड़ता है

  • PolarsResult और ? = Rust का Result + ? ऑपरेटर
  • जॉइन में &customers = उधार लेना (ओनरशिप लिए बिना पढ़ना)
  • Parquet लिखते समय &mut df = एक म्यूटेबल उधार
  • df!, col! स्टाइल मैक्रो = ! मैक्रो सिंटैक्स
  • स्कीमा और DataType = Rust का "हर चीज़ का एक ज्ञात प्रकार होता है" विचार, टेबल कॉलम पर लागू

डेमो

जानबूझकर अपूर्ण CSV

मिश्रित-प्रकार के कॉलम, एक नल, और एक खराब पंक्ति वाली फाइल का उपयोग करें:

order_id,customer_id,amount,status
1,100,42.50,shipped
2,101,,pending
3,102,17.00,shipped
4,bad_id,9.99,shipped

पंक्ति 4 में एक गैर-संख्यात्मक customer_id है। एक ढीले पाइपलाइन में यह एक साइलेंट NaN या ऑब्जेक्ट कॉलम बन जाता है। हम चाहते हैं कि यह स्पष्ट हो।

अनुमानित स्कीमा के साथ Eager रीड (आसान, खतरनाक रास्ता)

use polars::prelude::*;

fn main() -> PolarsResult<()> {
    let df = CsvReadOptions::default()
        .with_has_header(true)
        .try_into_reader_with_file_path(Some("orders.csv".into()))?
        .finish()?;

    println!("{df}");
    Ok(())
}

यह काम करता है — लेकिन अनुमान ने एक नमूना देखा और प्रकारों का अनुमान लगाया। एक अलग फाइल पर, या अधिक पंक्तियों पर, अनुमान बदल सकता है। अनुमान सुविधाजनक और गैर-निर्धारित है; वह संयोजन वही है जो आपको प्रोडक्शन में परेशान करता है।

स्पष्ट स्कीमा (विश्वसनीयता का पाठ)

अनुमान लगाना बंद करें। अनुबंध बताएं:

use polars::prelude::*;
use std::sync::Arc;

fn read_orders(path: &str) -> PolarsResult<DataFrame> {
    let mut schema = Schema::default();
    schema.with_column("order_id".into(), DataType::Int64);
    schema.with_column("customer_id".into(), DataType::Int64);
    schema.with_column("amount".into(), DataType::Float64);
    schema.with_column("status".into(), DataType::String);

    CsvReadOptions::default()
        .with_has_header(true)
        .with_schema(Some(Arc::new(schema)))
        .try_into_reader_with_file_path(Some(path.into()))?
        .finish()
}

अब customer_id को Int64 घोषित किया गया है। खराब पंक्ति (bad_id) अब टेक्स्ट के रूप में नहीं निकल सकती — Polars एक Err रिटर्न करता है, न कि चुपचाप दूषित कॉलम। विफलता स्पष्ट कारण के साथ रीड समय पर होती है, न कि तीन ट्रांसफॉर्मेशन बाद।

यह Rust + Polars का बिंदु है

  • स्कीमा कोड है — यह किसी भी अन्य अनुबंध की तरह वर्शन्ड, रिव्यूड और टेस्ट किया जाता है
  • finish() PolarsResult रिटर्न करता है। पार्स विफलता को गलती से अनदेखा करने का कोई तरीका नहीं है — ? आपको इसे संभालने या आगे बढ़ाने के लिए मजबूर करता है
  • एक डायनामिकली टाइप्ड पाइपलाइन की तुलना करें जहाँ एक खराब पार्स NaN बन जाता है और चुपचाप डाउनस्ट्रीम बह जाता है। यहाँ, टाइप सिस्टम और एरर टाइप चुप्पी को असंभव बना देते हैं।

प्रथम श्रेणी की चिंता के रूप में एरर हैंडलिंग

दोनों व्यवहार दिखाएं ताकि दर्शक अंतर महसूस कर सकें:

fn main() {
    match read_orders("orders.csv") {
        Ok(df) => println!("Loaded {} rows\n{df}", df.height()),
        Err(e) => eprintln!("CSV failed its contract: {e}"),
    }
}

एक पाइपलाइन में, Err का मतलब है कि जॉब यहाँ रुक जाती है, जोर से, एक संदेश के साथ — सुबह 3 बजे नहीं, चालीस मिलियन पंक्तियों के बाद।

एक टेस्ट के साथ लॉक करें

विश्वसनीयता का विषय ठोस हो गया — एक टेस्ट जो अनुबंध का दावा करता है, ताकि एक खराब अपस्ट्रीम फाइल CI में विफल हो जाए, न कि प्रोड में:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn schema_is_enforced() {
        let df = read_orders("tests/data/orders_good.csv").unwrap();
        assert_eq!(df.height(), 3);
        assert_eq!(
            df.column("amount").unwrap().dtype(),
            &DataType::Float64
        );
    }

    #[test]
    fn bad_types_are_rejected() {
        // `bad_id` वाली फाइल चुपचाप लोड नहीं होनी चाहिए।
        assert!(read_orders("tests/data/orders_bad.csv").is_err());
    }
}

bad_types_are_rejected एक टेस्ट में पूरा दर्शन है: हम दावा करते हैं कि खराब डेटा विफल होता है। अधिकांश पाइपलाइन वह टेस्ट कभी नहीं लिखती हैं क्योंकि उनके स्टैक में, खराब डेटा विफल नहीं होता — यह फैल जाता है।

जानबूझकर नल संभालना (गलती से नहीं)

पंक्ति 2 पर खाली amount एक वास्तविक नल है। तय करें कि इसका क्या मतलब है बजाय इसके कि एक अनुमान को तय करने दें:

use polars::prelude::*;

fn parse_options() -> CsvParseOptions {
    CsvParseOptions::default()
        .with_null_values(Some(NullValues::AllColumns(
            vec!["".into(), "NA".into(), "null".into()].into(),
        )))
}

परिचालन स्पष्टता: नल कोड में एक प्रलेखित निर्णय हैं, न कि पार्सर जो करना चाहता था उसका एक आर्टिफैक्ट।

अनुभाग के मुख्य निष्कर्ष

  • CSV डिफ़ॉल्ट रूप से अनटाइप्ड और असुरक्षित है — प्रत्येक रीड को एक सीमा के रूप में मानें जिसे मान्य किया जाना चाहिए
  • स्पष्ट स्कीमा "आशा है कि यह पार्स हो जाए" को "यह पार्स होता है या एरर देता है" में बदल देते हैं — सुविधा पर दृढ़ता
  • PolarsResult विफलता को अनदेखा करना कंपाइल-समय पर असंभव बना देता है
  • एक टेस्ट (bad_types_are_rejected) पूरे विश्वसनीयता थीसिस को प्रदर्शित करता है
  • Rust + Polars यहाँ इसलिए मायने नहीं रखता क्योंकि यह तेज़ है, बल्कि इसलिए क्योंकि यह साइलेंट डेटा भ्रष्टाचार को संरचनात्मक रूप से कठिन बनाता है