Снова привет! Мы – @cbmw и @AndreyErmilov, часть команды разработки «Рамблер/Медиа» (портал «Рамблер»). И это вторая часть наших размышлений по поводу сравнения Python и Rust (первая часть).
Если не хочется читать эту статью или лень возвращаться к первой части материала, можно посмотреть видео нашего выступления:
Многопоточность
Интересно, что использование типизации в Rust не ограничивается только тем, о чем мы говорили в первой части. Одна из областей, в которой хорошо видны ее преимущества, – многопоточное программирование.
Для начала давайте рассмотрим достаточно классический пример:
import threading
x = 0
def increment_global():
global x
x += 1
def taskof_thread():
for _ in range(50000):
increment_global()
def main():
global x
x = 0
t1 = threading.Thread(target=taskof_thread)
t2 = threading.Thread(target=taskof_thread)
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(5):
main()
print("x = {1} after Iteration {0}".format(i, x))
Этот код наглядно иллюстрирует ошибку «состояние гонки». Разберем чуть подробнее, что тут происходит. В коде мы видим запуск двух потоков, которые в цикле инкрементируют значение общего счетчика.
Нетрудно догадаться, что если один поток успевает записать новое значение в тот момент, когда другой поток уже считал старое значение счетчика, но не записал новое, то информация о записи первым потоком просто потеряется – ее перезатрет второй поток.
Надо проверить. Запустим код:
$ python race.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 86114 after Iteration 2
x = 58422 after Iteration 3
x = 89266 after Iteration 4
Видно, что 2-я, 3-я и 4-я итерации потеряли часть инкрементов.
Надо это исправить. Один из вариантов решения проблемы «состояние гонки» – это использование примитивов синхронизации, в данном случае попробуем использовать Mutex. Все, что нам нужно, – создать сам Mutex и передать его в качестве аргумента в каждый тред:
lock = threading.Lock()
t1 = threading.Thread(target=taskof_thread, args=(lock,))
t2 = threading.Thread(target=taskof_thread, args=(lock,))
Кроме того, нужно изменить саму функцию taskof_thread
, выполняющуюся в каждом из потоков:
def taskof_thread(lock):
for _ in range(50000):
lock.acquire()
increment_global()
lock.release()
Мы обернули вызов increment_global
функциями взятия и освобождения Mutex и тем самым заблокировали параллельное изменение счетчика из нескольких потоков. Теперь остальные треды будут ждать окончания выполнения инкрементации, если она уже была запущена в каком-либо потоке.
Проверим, что все починилось:
? python race.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
Да, теперь все работает корректно. Ну и по традиции рассмотрим этот пример в Rust. Если попробовать переписать Python-код «в лоб», получится следующее:
use std::thread;
static mut X: i32 = 0;
fn increment_global() {
X += 1;
}
fn thread_task() {
for _ in 0..50_000 {
increment_global()
}
}
fn main_task() {
let t1 = thread::spawn(thread_task);
let t2 = thread::spawn(thread_task);
t1.join().unwrap();
t2.join().unwrap();
}
fn main() {
for i in 0..5 {
main_task();
println!("x = {} after Iteration {}", X, i);
}
}
Этот код делает все то же, что и Python, за одним небольшим исключением – вы не сможете его скомпилировать.
Compiling playground v0.0.1 (/playground)
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> src/main.rs:6:5
|
6 | X += 1;
| ^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
--> src/main.rs:26:47
|
26 | println!("x = {} after Iteration {}", X, i);
| ^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0133`.
error: could not compile `playground`.
To learn more, run the command again with --verbose.
Компилятор нам прямо сообщает, что изменение переменной static mut
может привести к «состоянию гонки». Да, безусловно, мы можем заставить этот код скомпилироваться, обернув нужные вызовы в unsafe-блоки и переложив ответственность за проверку работы кода с компилятора на разработчика. Не будем приводить этот код тут, можете сами поэкспериментировать с этим. В таком случае код будет работать аналогично неисправленной Python-версии.
Тут явно видны различия в подходах при работе с многопоточным кодом в Python и Rust. Теперь попробуем исправить Rust-версию и проанализируем различия:
use std::{sync::{Arc, Mutex}, thread};
use lazy_static::lazy_static; // 1.4.0
lazy_static! { static ref X: Mutex<i32> = Mutex::new(0); }
fn increment_global(x: &Mutex<i32>) {
let mut data = x.lock().unwrap();
*data += 1;
}
fn thread_task(x: &Mutex<i32>) {
for _ in 0..50_000 {
increment_global(x)
}
}
fn main_task(x: &'static Mutex<i32>) {
let mut threads = vec![];
for _ in 0..2 {
threads.push(thread::spawn(move || thread_task(x)));
}
for thread in threads {
thread.join().unwrap();
}
}
fn main() {
for i in 0..5 {
main_task(&X);
let mut data = X.lock().unwrap();
println!("x = {} after Iteration {}", data, i);
}
}
Мы использовали аналогичный подход с использованием Mutex, но в тоже время тут скрывается важное отличие. В Rust Mutex – не просто отдельный тип, никак не связанный с теми данными, к которым он блокирует доступ. В Rust он представляет из себя обертку этих данных, и получить доступ к ним можно только путем взятия этого Mutex. Он отпускается автоматически при выходе из области видимости.
Такой подход исключает возможность по ошибке забыть заблокировать Mutex при доступе к данным или, например, заблокировать другой Mutex, который к этим данным отношения не имеет. В Python это остается вполне возможным, и контроль за этим лежит целиком на плечах разработчика.
Да, код становится чуть сложнее, но все еще вполне сопоставим с Python-вариантом. На наш взгляд, это является весьма малой платой за удобство и гарантии, предоставляемые компилятором Rust.
Еще стоит упомянуть, что threading.Lock
в Python можно использовать в качестве контекстного менеджера, что улучшит ситуацию, но не исправит ее полностью. Как минимум, он не позволит связать его с данными, которые защищены этим локом.
Но на этом различия в реализации многопоточности в Python и Rust не заканчиваются. Помимо примитивов, являющихся частью системы типов, компилятор предоставляет некоторые гарантии контроля возможности возникновения гонки данных и «состоянию гонки», хотя и не исключает последнее полностью.
Но и это еще не все. Rust предоставляет достаточно большой набор библиотек для работы с многопоточностью, и одна из них – это rayon. Не будем вдаваться в подробности работы, просто хотелось бы показать небольшой пример распараллеливания кода с ее использованием.
Последовательный код:
fn main() {
let mut arr = [0, 7, 9, 11];
arr.iter_mut().for_each(|p| *p -= 1);
println!("{:?}", arr);
}
Параллельный код:
use rayon::prelude::*;
fn main() {
let mut arr = [0, 7, 9, 11];
arr.par_iter_mut().for_each(|p| *p -= 1);
println!("{:?}", arr);
}
Асинхронность
Подход, описанный выше, применим и при работе с асинхронным кодом. В целом, если говорить об асинхронщине более обобщенно, можно назвать подходы в Python и Rust достаточно похожими. Рассмотрим небольшой пример кода, который (абсолютно) бесполезен на Python:
import asyncio
async def timers():
futures = []
for s in range(3):
futures.append(asyncio.sleep(s))
await asyncio.gather(*futures)
if __name__ == "__main__":
asyncio.run(timers())
И на Rust:
use tokio::time::{delay_for, Duration};
use futures::future::join_all;
async fn timers() {
let futures = (0..2)
.map(Duration::from_secs)
.map(delay_for);
join_all(futures).await;
}
#[tokio::main]
async fn main() {
timers().await;
}
По коду можно заметить сходство в использовании методов и построении асинхронного кода. Мы сознательно не будем погружаться в различия и сходства реализации асинхронщины, сейчас нам интересно рассмотреть это на более высоком уровне. В целом, создается впечатление, что перейти с Python на Rust не составляет большой проблемы, и, отчасти, это правда. Однако, так было не всегда: привычный Python разработчикам async/await появился лишь в стабильной версии 1.39.0, вышедшей 7 ноября 2019 года, то есть чуть больше года назад. До этого асинхронный код в стабильном Rust представлял из себя последовательность из комбинаторов:
fn main() {
let addr = "127.0.0.1:1234".parse().unwrap();
let future = TcpStream::connect(&addr)
.and_then(|socket| {
io::write_all(socket, b"hello world")
})
.and_then(|(socket, _)| {
// read exactly 11 bytes
io::read_exact(socket, vec![0; 11])
})
.and_then(|(socket, buf)| {
println!("got {:?}", buf);
Ok(())
})
.map_err(|_| println!("failed"));
tokio::run(future);
}
Что выглядит для уже привыкших к async/await Python-разработчиков, достаточно чужеродно. Однако в таком подходе есть и плюсы, и минусы, и, вероятно, многие, сталкивающиеся с функциональными языками, увидят в этом много знакомого. Стоит отметить, что комбинаторы доступны и сейчас, наряду с await
вы вполне можете сочетать оба этих подхода. Помимо всего описанного выше в Rust, в отличие от Python, широко используется концепция каналов.
Из сходств можно отметить множественность реализаций event loop
-ов. Правда, ни один из языков не лишен проблем при написании кода, абстрагированного от библиотек, реализующих event loop.
Функциональная парадигма
В тексте выше мы не раз уже касались функциональных возможностей языков, и вот настало время посмотреть на это чуть пристальнее. На полноту и объективность тут рассчитывать не приходится – сама по себе тема слишком бездонна, но попробуем оценить ее в разрезе нашего опыта.
Функции высшего порядка
Это первая концепция, которая присутствует и в Python, и в Rust. Как уже стало привычным по ходу статьи, попробуем сравнить несколько вариантов кода:
from typing import List, Callable
def map_each(list: List[str], fun: Callable[[str], int]) -> List[int]:
new_array = []
for it in list:
new_array.append(fun(it))
return new_array
if __name__ == '__main__':
languages = [
"Python",
"Rust",
"Go",
"Haskell",
]
out = map_each(languages, lambda it: len(it))
print(out) # [6, 4, 2, 7]
Выше в примере Python мы сразу использовали модуль typing
и, соответственно, описали сигнатуру функции map_each
, выполняющую следующее: она принимает два аргумента, один из которых – список со строками, а второй – Callable
-объект, или говоря проще, функция. Она принимает в качестве аргумента строку, а возвращает – число. Выглядит все достаточно приятно.
Посмотрим, что в Rust:
fn map_each(list: Vec<string>, fun: fn(&String) -> usize) -> Vec<usize> {
let mut new_array: Vec<usize> = Vec::new();
for it in list.iter() {
new_array.push(fun(it));
}
return new_array;
}
fn main() {
let list = vec![
String::from("Python"),
String::from("Rust"),
String::from("Go"),
String::from("Haskell"),
];
let out = map_each(list, |it| it.len());
println!("{:?}", out); // [6, 4, 2, 7]
}
Выглядит похоже, но есть одно, важное различие: Python не поддерживает многострочные lambda-функции. Это резко ограничивает выразительность и возможности всего функционального подхода. Да, безусловно, это можно пережить, пользуясь, например, обычными функциями, но удобства это определенно не добавляет.
Кроме того, важным отличием являются принципы реализации «комбинаторов». В Rust большая часть из них – часть трейта Iterator, тогда как в Python это, как правило, самостоятельные функции. Исходя из вышеописанного и того, что трейт Iterator реализован для многих стандартных типов и может быть реализован для ваших типов, вытекает и различие в использовании этих методов. В Rust они достаточно органично объединяются в цепочки, описывая сложные трансформации данных.
fn main() {
let list = vec![
Ok(42),
Err("Error - 1"),
Ok(81),
Ok(88),
Err("Error - 2")
];
let out: Vec<_> = list
.iter()
.flatten()
.collect();
println!("{:?}", out); // [42, 81, 88]
}
Выше пример кода, отбрасывающего ошибочные результаты из вектора и преобразующего их в вектор целочисленных значений. Данный код, собственно, строится на том, что уже знакомый нам Result реализует трейт IntoIterator. Еще один пример, иллюстрирующий возможности Rust:
fn main() {
let output = ["42", "43", "sorokchetire"]
.iter()
.map(|string| {
string
.parse::<i32>()
.map_err(|_| println!("Parsing error"))
})
.flatten()
.map(|integer| integer * 2)
.fold(0, |base, element| base + element);
println!("{:?}", output);
}
Этот код уже делает чуть больше: происходит итерация по массиву со строками, попытка преобразования каждого элемента массива в i32 и, в случае ошибки, вывода сообщения об ошибке в консоль. Далее происходит отбрасывание некорректных результатов, умножение каждого значения на два и сложение полученных элементов.
В чем же прелесть таких цепочек? На мой взгляд, это читаемость и простота поддержки. Представим, что нам нужно добавить дополнительное преобразование в пример выше перед вызовом .fold
. Очевидно, что добиться этого достаточно просто дополнительным вызовом map. Да, в Python есть концепция Comprehensions, но производить какие-то достаточно сложные манипуляции с данными внутри самого comprehension не совсем удобно.
Замыкания
Сама концепция известна и достаточно широко используется и в Python, и в Rust. Вот только система типов Rust позволяет делать некоторые проверки, недоступные в Python. По традиции рассмотрим пример, и на этот раз начнем с Rust:
fn main() {
let mut holder = vec![];
let sum = |a: usize, b: usize| -> usize {
let c = a + b;
holder.push(a + b);
c
};
println!("{}", sum(10, 20));
}
Выше мы видим код, описывающий замыкание sum
, которое, в свою очередь, принимает два параметра и складывает их. Результат сложения записывается в вектор holder
и возвращается из замыкания. Важно отметить, что holder
является внешним по отношению к самому замыканию, то есть он олицетворяет некоторый глобальный стейт в этом примере. Попробуем скомпилировать и увидим ошибку:
error[E0596]: cannot borrow `sum` as mutable, as it is not declared as mutable
То есть компилятор запрещает нам изменять внешнее состояние в замыкании пока само замыкание не будет объявлено как mut
. Исправляется это достаточно банально:
fn main() {
let mut holder = vec![];
let mut sum = |a: usize, b: usize| -> usize {
let c = a + b;
holder.push(a + b);
c
};
println!("{}", sum(10, 20));
}
Да, полностью отличить чистые функции на уровне типов это не позволяет, но зато можно понять, какие из замыканий явно изменяют внешнее состояние при своем вызове, что уже хорошо. Хотя, возможно, многим хотелось бы большего. Ситуация тут в целом схожа с ситуацией с изменяемыми и неизменяемыми типами в Rust и Python. В случае Rust компилятор явно просит указывать, какие конкретные наборы данных будут изменяемыми, а какие – нет. Что кажется чуть более гибким подходом, чем тот, что используется в Python, где изменяемость определяется самим типом данных и, по сути, изменить ее нельзя.
Рекурсия
И это последняя концепция, которую мы возьмем на рассмотрение. В целом ситуация с рекурсией схожа в обоих языках. Отсутствие оптимизации хвостовой рекурсии характерно и Rust, и Python. Однако llvm (backend rust) позволяет генерировать код с учетом этой оптимизации. Вполне возможно, что однажды в Rust появится и эта оптимизация с гарантированным применением при соблюдении всех условий. Кроме того, различия кроются непосредственно и в ошибках при рекурсивных вызовах. В Python RecursionLimit
является обычным исключением и позволяет при необходимости перехватить его и продолжить выполнение приложения. В Rust при превышении уровня вложенности рекурсивных вызовов может произойти ошибка переполнения стека.
Заключение: Зачем-же питонисту Rust
Как мы уже говорили, питонисту очень многое приходится делать самому — проверять типы, держать в голове, где будет None, а где число, искать в документации, какие исключения может выбросить функция. Это сложно и поэтому возникает большая часть ошибок, о которых мы говорили в начале. Mypy помогает избавиться только от их части, и компилятор Rust сильно выигрывает в этом у mypy, но при этом можно столкнуться с другими подводными камнями, которые мы пока не рассматривали.
У Rust достаточно высокий порог входа. Для его использования нужно осознать многие концепции, которые мы описали выше, и те, которых пока не касались. И некоторые из них действительно сложные. Спустя некоторое время начинаешь осознавать эти концепции, они уже не кажутся такими сложными и не так сильно замедляют процесс разработки. Но это время должно пройти, и этот порог необходимо преодолеть.
В итоге мы пришли к следующему выводу: разработка приложения на Python действительно привлекательна своей высокой скоростью. Но потом, когда разработанное приложение нужно поддерживать, вылезают все те проблемы, которые были проигнорированы во время разработки. В то же время Rust не позволяет игнорировать возможные варианты, требуя больше времени на разработку, но и благодаря этому потом тратится гораздо меньше усилий на поддержку и рефакторинг.
Vest
Добрый день, спасибо за статью, было интересно взглянуть на ваши примеры своими глазами. От себя я хотел бы добавить комментарий. Я нисколько не профессиональный разработчик Rust, просто решал Адвент календарь, чтобы немножечко выучить язык (я до этого его не знал совсем).
Так вот, если вы не против, я бы посоветовал свой вариант для примера с функциями высшего порядка:
cbmw Автор
Спасибо большое за комментарий! Ваши примеры выглядят более "rust"-way, если можно так сказать, и, безусловно, имеют место быть.
В самой статье, примеры на rust иногда, в некоторых местах, сознательно старались приблизить к python версии, чтоб сконцентрировать внимание на определённых деталях и упростить понимание того, что происходит в коде для тех кто с rust не знаком.
Vest
В этом плане вы правы. Просто когда я работал с одними и теми же действиями (разбей на строки, обрежь пробелы и преврати в число), то хотелось бы это сделать лаконичнее :) а потом уже без создания вектора, лишь на чистых итераторах.
freecoder_xx
Ссылка на
parse
во второмmap
не нужна. Ну и первыйmap
делает ненужную аллокацию:Vest
Спасибо. Я пробовал как-нибудь избежать использования ToOwned, но не смог подобрать нужное описание функции (я хотел не использовать замыкание в этом случае), но не смог избежать &&str, когда мне нужен был &str.
А по поводу sum, я смею предположить, что вы используете itertools крейт.
lonely_guy
"std::iter::Sum — Rust" https://doc.rust-lang.org/std/iter/trait.Sum.html#tymethod.sum
Он вполне себе в стд либе
Vest
Упс. Значит я лох :( Я подумал об этом методе, но писал код в интернете (без автодополнения) и не поискал заранее.
Извините.
freecoder_xx
Проблему с двойной ссылкой нельзя обойти из-за того, что массивы в Rust пока нельзя преобразовать в итератор по-значению, а не по-ссылке. Но вскоре это будет исправлено. Тогда можно будет писать так:
AnthonyMikh
В данном случае — можно, достаточно после
.iter()
воткнуть.copied()
Zanak
Нельзя сравнивать несравнимое, а тем более противопоставлять.
Бессмысленно превозносить компилируемые языки за то, что они выполняют проверку всего кода программы в процессе сборки. У скриптовых языков этот момент просто отсутствует.
Бессмысленно превозносить поддержку многопоточности в раст, она конечно круче питонячей, потому что питон изначально не создавался для потоков. Пока vm питона не научат атомарно исполнять его байткод и нормально масштабироваться по ядрам процессора его потоки так и останутся приделанным сбоку.
На счет функциональщины и замыканий — не убедили. Писать в функциональной манере можно, коли пришла такая охота, и кому это интересно, те знают про functools. Другое дело — это вопрос целесообразности. Питон, все таки ОО язык, и профит от чуждой парадигмы, лично для меня, не очевиден.
Финальная ремарка вообще убила: «Зачем-же питонисту Rust». Затем, зачем и все другие компилируемые языки, реализовать то, что сам питон делает плохо, например, распараллелить и/или ускорить какую нибудь задачу.
freecoder_xx
Дело вовсе не в производительности готовых программ, дело в производительности разработки. Вы не уловили главный вывод этого доклада: на Python получается быстрее разрабатывать с нуля, но дорабатывать и поддерживать систему, тем более крупную, быстрее и легче на Rust, так как он кучу важных проверок выносит на этап компиляции. В этой плоскости языки можно и нужно сравнивать, чтобы стало понятно, что лучше взять для быстрой разработки прототипа, который собрал и выкинул, а что лучше взять для разработка MVP, который предполагается дальше развивать продолжительное время.
DirectoriX
Это справедливо вообще для любой пары из скриптового и компилируемого языка
freecoder_xx
Как вы думаете, какую систему будет проще дорабатывать и поддерживать, написанную на Python или на C?
DirectoriX
Судя по тому, что C прилично больше в продакшене, чем Python — всё-таки C
Crandel
И в чем же преимущество возраста С? Это же не вино или коньяк
DirectoriX
Понятия не имею, вы откуда-то взяли возраст — вы и отвечайте.
UPD:
Знаете, COBOL тоже не алкогольный напиток, но от него избавляться не хотят / не могут, в частности из-за того, что программы многолетней «выдержки» очень уж хорошо отлажены, а заново написанные — неизвестно.
Crandel
Перепутал с дольше, но разница невелика
Тем не менее я не видел ни курсов, ни желающих изучать его, хоть он и востребован в определенных местах
DirectoriX
О, разница существенна.
Я в продакшене (в самом широком смысле) с Python-скриптами (в любом виде) сталкиваюсь прилично реже, чем с C-кодом. Беру телефон в руки — там Python'ом вообще не пахнет (правда и C не так много, но хотя бы в самой ОС имеется), смотрю видео или слушаю музыку в браузере не на Python, а на C (а ещё кодеки, кодеки уж точно на C или вовсе ассемблерные!), как редактор какой запустить — там и LibreOffice, и Notepad++, и VS Code, и тем более Qt Creator в основном на плюсах… На работе Python видел только в форме небольших утилитарных скриптов (по уровню функционала — как раз прототипы), всё остальное — C. Хотя вру — изредка приходится обучать нейронку с помощью TensorFlow — но потом всё равно веса экспортируем в C-хедер, потому что бекенд-то Сишный
freecoder_xx
Тем не менее, большая часть сайтов в Интернете написана на PHP. А в браузере у вас выполняется в основном JavaScript. А вот на чем написан типичный браузер: https://4e6.github.io/firefox-lang-stats/ Доля именно C там не подавляющая. Мобильные приложения — это Java и Kotlin (или Swift).
DirectoriX
У нас же JS магическим образом нативно выполняется, совсем не в JS-машине, корторая написана на компилируемых языках типа C/C++/Rust (то же самое про PHP).
А ещё по вашей ссылке выходит, что в Firefox больше используется HTML, чем C — вы тоже скажете, что он «выполняется»?
freecoder_xx
Вы с темы не съезжайте. Речь шла именно про язык С. Если вам нужно написать приложение для браузера, вы все еще возьмете JavaScript (ну или в недалеком будущем Rust, но сейчас не об этом) и вам все равно, на каком языке написан его интерпретатор. Я не умоляю значения языка С, но я сильно сомневаюсь в вашем утверждении, что любой компилируемый язык (для примера С) подходит лучше для разработки сложных систем любого другого интерпретируемого языка (Python, PHP, JavaScript).
DirectoriX
WebAssembly.
Окей, расшифрую. Внезапно, WebAssembly позволяет писать фронтенд на C.
А ещё, браузерный фронтенд — это не весь-весь-весь продакшен-код, есть и бекенд, и всё, что вообще не связано с браузерами — и там JS не очень любят (ну, есть, конечно, NodeJS, но её используют в основном фронтендеры для сборки своего проекта, или кто «АААА, бекенд надо, а я кроме JS не знаю ничего»).
К счастью, подавляющее большинство софта всё ещё написано на компилиуемых языках — на своём домашнем компьютере я явно ни одно приложение на скриптовых языках не запускаю. А вот C/C++/.Net/Java приложения — запускаю, по многу раз каждый день. Вы опять скажете про браузеры — ну что поделать, если W3C не добавляет поддержку компилируемых модулей (кроме того же WebAssembly).
AnthonyMikh
Но чем тут хорош именно сам COBOL — непонятно
DirectoriX
Ничем не хорош, просто слишком уж важное наследие. Сегодня на нём, конечно, никто не захочет писать кроме совсем лютых гиков.
eumorozov
Практически не встречал программ, которые не требуют изменений. Точнее, один раз на одном предприятии видел пару PDP-11, которые не выключались десятилетиями, потому что там запускалась какая-то важная программа, уже не помню для чего, а исходных лент/дисков с ней не осталось. У сотрудников предприятия были обоснованные опасения, что если выключить компьютеры, то больше они не включатся, а если включатся, то неоткуда будет загрузить программу. На мониторах были выжжены какие-то стандартные приглашения ОС для PDP-11, хотя разглядеть хоть что-либо на этих мониторах было непросто.
Но это один вырожденный случай, честно говоря, в тот момент не догадался выпытать, что это за программа, в которой за несколько десятилетий не понадобилось внести ни одной правки. Она была запущена в одной стране, а продолжала выполняться в другой.
Думаю, что таких случаев исключительно мало. Всё вокруг нас непрерывно меняется. Законы, налоги, курсы валют, да всё подряд. Как в этом непрерывном хаосе можно 20-30-40 лет использовать один и тот же код — почти не представляю. Разве что для каких-то совершенно тривиальных задач.
Zanak
Скриптовые языки для того и придумывались, чтобы на них было быстрее разрабатывать, вынося все тяжелые вещи на плечи, в первую очередь, С. Выступая клеем, который позволяет легко собирать и комбинировать реализацию необходимых алгоритмов на «серьезных» языках, скриптовые языки дают гибкость в реагировании на конечные требования.
Оглядываясь на обе статьи вместе, у меня сложилось впечатление, что автор просто неправильно выбрал инструмент, и теперь очень расстроен, что он оказался не достаточно, для него, универсален.
PS Всех с наступающим новым годом.
freecoder_xx
Быстрее разрабатывать именно крупные системы, заметьте. Делать
a + b
вообще говоря все равно, на каком языке. А вот когда вы можете на высоком уровне оперировать множеством компонентов и легко связывать их в нечто более крупное… Но потом это крупное нужно поддерживать. Но потом это крупное требует особого тюнинга и нужно спускаться на уровень ниже. И та легкость и незамутненность типами, которая помогала на первом этапе, вдруг становится серьезным тормозом дальнейшего развития системы.Bruce_Robertson
Важное уведомление!
Уважаемые хабравчане, не забывайте, что Рамблер груп — это те самые гопники, которые буквально год назад, организовали уголовное преследование своего бывшего сотрудника Игоря Сысоева (создателя NGINX), пытаясь отжать у него, его же детище. А потом, когда это не удалось, для отмыва репутации передала требования другой, подконтрольной компании, которая продолжила судебную травлю, но уже в международном формате. Плюсуя посты в данном блоге — вы помогаете этой гоп-конторе продолжать тут пиариться, как будто ничего и не было.
RamblerGroup я буду напоминать об этой подлости под каждым вашим постом и пофиг что меня скорее всего будут минусить. В вашем случае было бы логичным
застрелитьсяпрекратить активность на Хабре. Ну либо выкатить в опенсорс инструмент, который будет полезен всему миру.freecoder_xx
Вообще по отношению к опенсорсу и величине вклада в него вполне можно судить об адекватности и прогрессивности ИТ-компании сегодня, я думаю.