Разъяснение разработки на основе REPL для разработчиков C++/Java и C#
Если вы используете один из основных статически типизированных языков, таких как C++, Java или C#, вы, вероятно, привыкли использовать большую сложную IDE. Может показаться чрезвычайно примитивным, как много людей работают с динамическими языками, такими как Python, Ruby или Julia, просто используя командную строку и текстовый редактор.
Я попытаюсь объяснить преимущества такой разработки на нескольких практических примерах, чтобы сравнить ее с IDE.
Ваш язык также является вашим инструментом развития
В статически типизированных языках язык и инструменты, которые вы используете для разработки кода, — это две совершенно разные вещи. Если вы программируете на C++, вы не используете код C++ для помощи в разработке. Скорее вы используете IDE для поиска использования функций, иерархий типов, существующих методов для данного типа и т. д. Для настройки системы сборки, добавления библиотек и т. д. вы используете свою IDE, а не C++. Возможно, звучит странно, что я упоминаю об этом, потому что для разработчика Java или C++ это самоочевидно.
Однако для многих разработчиков динамических языков это принципиально другое. Большинство этих задач, которые выполняет IDE, вместо этого обрабатывается самим языком. В основном я использую Джулию, поэтому я буду использовать ее в качестве примера.
Добавление сторонних пакетов или библиотек
Разработчик C++ загрузит стороннее программное обеспечение, а затем ему придется использовать IDE, чтобы добавить его в свой проект. В IDE они будут настраивать параметры компиляции и т. д. Однако, когда я запускаю REPL Julia, я делаю все это внутри самой Julia. На подсказку Юлии напишу:
julia> Pkg.add("LightXML")
Это приведет к загрузке и установке пакета LightML
. Если я хочу использовать пакет в своем коде, я пишу в своем исходном коде:
using LightXML
Язык используется во всех аспектах разработки пакетов. Пользователь IDE, скорее всего, щелкнет какой-нибудь мастер, чтобы решить, какой проект создать, написать имя проекта и получить сгенерированный шаблон. В Julia, если я хочу создать пакет с именем Foobar
с лицензией MIT
, я пишу в REPL Julia:
julia> PkgDev.generate("Foobar", "MIT")
Если я в какой-то момент захочу запустить тест пакета, включая мой собственный, я пишу:
julia> Pkg.test("Foobar")
Я мог бы показаться медленным, чтобы писать эти полные команды для выполнения задач, которые вы выполняете постоянно. Открытие какой-либо панели и нажатие кнопок может показаться быстрее. Однако большинство сред REPL имеют множество средств для сокращения набора текста за счет выполнения команд, использования истории ранее выпущенных команд и т. д.
Например, я могу просто написать:
julia> Pkg.
и нажмите клавиши со стрелками вверх или вниз, чтобы просмотреть все предыдущие команды, которые я ввел, которые начинаются с Pkg.
. В качестве альтернативы я могу использовать вкладку, чтобы получить предложения по функциям, существующим в модуле Pkg
.
В качестве альтернативы, если я не помню начало своей команды, а просто хочу просмотреть все команды со словом «Foobar», внутри него я могу нажать Ctrl+R и Ctrl+S, чтобы переключаться между операторами, содержащими «Foobar». .
Получить информацию о типах и функциях
Большинство современных IDE имеют функциональные возможности, которые позволяют:
- перейти к определению функции или типа
- получить документацию о функции или типе
- узнать о подтипах или подклассах
- помочь вам найти методы, доступные для объекта
Чтобы получить всю эту функциональность, создатели или ваша IDE должны ее специально создать.
Сравните это с динамическим языком, использующим REPL. Вы можете делать большинство из этих вещей, не полагаясь на то, что кто-то создаст для этого большую IDE. Когда вы загружаете код в REPL, динамический язык создает структуру данных для всего анализируемого кода, которая дает информацию о том, к какому модулю он принадлежит, из какого файла исходного кода он взят и т. д.
Поиск определений функций
Это упрощает добавление функций и макросов к самому языку для изучения кода различными способами. Например. в Джулии я могу написать:
julia> @edit length("hello")
Это откроет мой предпочтительный редактор (можно настроить) в месте, где определена функция length()
. Мало того, он рассмотрит тип аргумента и выберет функцию length()
, обрабатывающую строки, а не одну, обрабатывающую массивы, например.
Или я мог бы использовать макрос @less
, чтобы увидеть код прямо в моем REPL:
julia> @less length("hello")
Интересно то, что @edit
— это макрос в стандартной библиотеке, использующий функцию edit()
, которая состоит примерно из 100 строк кода. Так что написать эту функцию несложно, если она еще не существует.
Документация по поиску
Джулия REPL написана на Джулии и может быть легко расширена с помощью кода Джулии. Он поддерживает разные режимы. Тот, который я показывал вам, дает вам подсказку julia>
. Однако вы можете добавить любое количество других режимов. На самом деле в Джулии уже есть два других встроенных режима. Если я нажму на символ вопросительного знака, моя подсказка изменится на help>
, я могу использовать это, чтобы получить справку по любой функции.
help?> println
search: println print_with_color print print_shortest sprint @printf isprint @sprintf
println(io::IO, xs...)
Print (using print) xs followed by a newline. If io is not supplied, prints to STDOUT.
Хорошая вещь в том, что это работает с любой документацией, которую я пишу.
julia> "add two numbers" my_add(a::Number, b::Number) = a + b
my_add
help?> my_add
search:
add two numbers
Как видите, все, что мне нужно сделать, это добавить строку перед определением функции. После этого он сразу становится доступным в справочной системе.
Получить информацию о типах
В IDE обычно есть различные инспекторы, чтобы показать вам информацию о типах, таких как их поля, подклассы, суперклассы и т. д. На самом деле в IDE, которую я обычно использую для разработки на C++, Qt Creator, большая часть этой функциональности настолько медленная, что я ею не пользуюсь. если мне действительно не нужно.
Опять же, в динамическом языке, таком как Julia, я могу просто использовать простые функции для решения этой проблемы. Скажем, библиотека определяет такой тип:
struct Point
x::Int
y::Int
end
Я хочу знать поля, которые имеет тип. Тогда я могу написать:
julia> fieldnames(Point)
2-element Array{Symbol,1}:
:x
:y
Я мог легко смотреть на подтипы и супертипы:
julia> supertype(Int64)
Signed
julia> subtypes(Signed)
5-element Array{Union{DataType, UnionAll},1}:
Int128
Int16
Int32
Int64
Int8
При таком подходе к работе с кодом важно понимать, что, поскольку это всего лишь функции на том же языке, на котором вы разрабатываете и с которым уже знакомы, расширять их несложно. Мы можем повторно использовать функцию subtype()
для создания более сложной функциональности. Например. вот функция из Викикниги Юлии.
julia> function showtypetree(T, level=0)
println("\t" ^ level, T)
for t in subtypes(T)
if t != Any
showtypetree(t, level+1)
end
end
end
Я могу использовать это, чтобы показать дерево подтипов.
julia> showtypetree(Integer)
Integer
BigInt
Bool
Signed
Int128
Int16
Int32
Int64
Int8
Unsigned
UInt128
UInt16
UInt32
UInt64
UInt8
Завершение метода
Завершение метода — одна из киллер-фич IDE, которую, как мне кажется, чаще всего упоминают поклонники статической типизации в качестве причины для работы с IDE и языком со статической типизацией. Я могу согласиться с тем, что на самом деле это преимущество статически типизированных языков.
Однако мы не совсем потерялись в море и с языком с динамической типизацией. Именно здесь действительно проявляется значение разработки на основе REPL. Если вы пишете код в основном в простом текстовом редакторе, вы будете несколько ограничены. Однако в моем стиле разработки я часто прыгаю между текстовым редактором и REPL. Я часто загружаю код в свой REPL, просто закрывая REPL и перезапуская его с помощью:
$ julia -i my-source-code.jl
Или я просто загружаю файл в REPL с помощью:
julia> include("my-source-code.jl")
На самом деле существует довольно много способов сделать это. Можно также указать, что весь модуль также должен быть перезагружен.
Это означает, что, поскольку я разрабатываю свой REPL, большая часть моего кода работает. Джулия проанализировала его и сохранила его представление в памяти, что означает, что его можно запрашивать во время выполнения различными способами. Например. Я мог бы захотеть узнать все функции, которые принимают аргумент типа Kelvin
:
julia> methodswith(Kelvin)
9-element Array{Method,1}:
gas_mass(P::Real, V::Real, T::Airship.Kelvin, molecular_mass::Real) in Airship at gaslaws.jl:33
gas_moles(P::Real, V::Real, T::Airship.Kelvin) in Airship at gaslaws.jl:26
gas_volume(n::Real, T::Airship.Kelvin, P::Real) in Airship at gaslaws.jl:20
*(t::Airship.Kelvin, c::Airship.GasConstant) in Airship at gaslaws.jl:8
*(c::Airship.GasConstant, t::Airship.Kelvin) in Airship at gaslaws.jl:9
+(x::Airship.Kelvin, y::Airship.Kelvin) in Airship at temperature.jl:41
-(x::Airship.Kelvin, y::Airship.Kelvin) in Airship at temperature.jl:42
convert(::Type{Airship.Celsius}, kelvin::Airship.Kelvin) in Airship at temperature.jl:21
convert(::Type{Airship.Fahrenheit}, kelvin::Airship.Kelvin) in Airship at temperature.jl:24
Конечно, вы можете запросить это у объекта так, как это делает IDE. Я могу быстро написать функцию, чтобы сделать это:
julia> completions(obj) = methodswith(typeof(obj))
Все виды служебных функций, подобных этой, могут быть сохранены в файле запуска Julia, поэтому вы можете использовать его с любым проектом. Теперь, если у меня есть переменная, содержащая такую температуру:
julia> temp = Celsius(4)
Airship.Celsius(4.0)
Я могу быстро проверить, какие функции доступны для этого объекта, с помощью:
julia> completions(temp)
7-element Array{Method,1}:
gas_mass_CO2(P, V, celsius::Airship.Celsius) in Airship at densities.jl:30
gas_mass_N2(P, V, celsius::Airship.Celsius) in Airship at densities.jl:31
gas_mass_O2(P, V, celsius::Airship.Celsius) in Airship at densities.jl:32
+(x::Airship.Celsius, y::Airship.Celsius) in Airship at temperature.jl:38
-(x::Airship.Celsius, y::Airship.Celsius) in Airship at temperature.jl:39
convert(::Type{Airship.Kelvin}, celsius::Airship.Celsius) in Airship at temperature.jl:19
convert(::Type{Airship.Fahrenheit}, celsius::Airship.Celsius) in Airship at temperature.jl:23
На самом деле это сложнее, чем то, что вы получаете в типичной среде IDE, поскольку вы можете найти все функции, в которых любой аргумент имеет заданный тип, а не только первый.
И, конечно же, в самом REPL у нас уже есть автодополнение, основанное на типах, модулях и функциях, живущих в среде REPL. Если я начну писать, например. split
и нажмите Tab, это происходит:
julia> split
split splitdir splitdrive splitext
Это также работает для завершения типов и полей типов. Итак, скажем, я определил этот класс прямоугольника:
julia> struct Rect
x::Int
y::Int
width::Int
height::Int
end
julia> r = Rect(10, 10, 40, 40)
Rect(10, 10, 40, 40)
julia> r.
height width x y
Отладка
Когда я начал использовать динамические языки, я какое-то время использовал в IDE языки со статической типизацией. Я привык к сложным отладчикам, в которых я мог проходить код по одной строке за раз и иметь окно, показывающее мне список переменных и позволяющее мне их проверить.
Поначалу меня смущало кажущееся отсутствие отладчиков в большинстве динамических языков. Конечно, эта ситуация сильно различается, и это было в динамических языках, таких как Smalltalk и Racket, где меня больше всего впечатлила отладка, хотя она выглядит очень простой.
Smalltalk обычно поставляется с интегрированной средой разработки (IDE), хотя и выглядит простой по стандарту статической типизации. У Racket есть простая IDE под названием DrRacket.
Обычно они позволяют выполнять код пошагово. Прелесть их, однако, в том, что как только вы остановитесь на строке, вы не будете ограничены функциями, предоставленными поставщиками IDE для проверки и изменения кода. В динамических языках код представляет собой живой организм, поэтому вы можете проверять любой объект, который хотите, используя код и соглашения, к которым вы привыкли. Вы можете добавить или изменить любую понравившуюся вам функцию в процессе отладки. Изменение состояния переменной, чтобы увидеть, заставит ли это код работать правильно, — это простое присвоение переменной, как и любое другое.
Это преимущество отсутствия четкого разделения времени компиляции и времени выполнения, как в большинстве статических языков. Ваш программный код легко модифицируется во время выполнения на динамическом языке.
Для Julia у вас есть пакет Gallium.jl
, который просто расширяет REPL Julia и предоставляет еще один режим, позволяющий выполнять отладку. Однако использование этого отладчика ничем не отличается от установки и использования любого другого пакета.
julia> Pkg.add("Gallium")
julia> using Gallium
Как я показал вам ранее, функциональность обеспечивается простым добавлением обычных функций и макросов Julia. Если я хочу войти в функцию и посмотреть, что она делает, я просто использую макрос @enter
:
julia> @enter join(["hello", "world"], " ")
Это перейдет к выполнению функции join()
и позволит вам пройти через нее.
Однако обычно я редко использую отладчики с динамическими языками. С разработкой на основе REPL вы учитесь разрабатывать с другой философией. Я пишу очень короткие функции, которые носят функциональный характер. Это означает, что я постоянно пробую и тестирую функции по мере их написания. Это очень легко сделать в среде REPL. Я могу легко вставлять или записывать подмножества функций в REPL, чтобы попробовать их.
Сегмент кода, который, как вы подозреваете, не работает так, как должен в C/C++, — это то, что я бы просто выделил в отдельную функцию в Julia и сразу же протестировал в REPL. Другими словами, вы обычно обнаруживаете проблемы в интерфейсе функции.
Расширение ваших инструментов
Вы можете думать о REPL как о своей IDE. В отличие от традиционной IDE, ее можно расширить простыми функциями, написанными на том же языке, на котором вы программируете.
Скажем, я хочу переключиться между случаем с верблюдом и чехлом со змеей. Я пишу простую функцию Julia, которая делает это, читая буфер обмена и записывая обратно в буфер обмена. Например, чтобы превратить случай змеи, подобный этому hello_world
, в случай верблюда, подобный этому HelloWorld
, я могу сделать это в одной строке.
camel_case(s::AbstractString) = join(capitalize.(split(s, "_")))
Затем я определяю другую версию, которая читает буфер обмена и затем записывает в него.
function camel_case()
s = camel_case(clipboard())
clipboard(s)
s
end
Итак, парой строк я добавил еще одну функцию в свою «IDE». Я пишу множество таких маленьких функций для всевозможных задач, которые я решаю во время разработки. Например. вы можете добавить функции для типичных операций, выполняемых с помощью git.
Проблема с этим в обычной IDE заключается в том, что функции нельзя вызывать как есть. Вам нужно написать много связующего кода для взаимодействия с остальной частью IDE и преобразования ввода пользователя. Простой маршалинг ввода и вывода функции camel_case()
в обычной среде IDE потребовал бы намного больше строк кода, чем все ядро функции.
Не пишите скрипты, пишите функции
Должен признаться, я тоже довольно поздно пришел к такому ходу мыслей. До того, как я использовал Джулию, я привык писать небольшие скрипты на Python или Go, которые я вызывал из оболочки. Проблема с этим подходом в том, что мне пришлось написать много кода, чтобы просто проанализировать аргументы в оболочке. Кроме того, в обычной оболочке Unix нет ничего особенно легкого для сложной компоновки, поскольку здесь нет объектов, только простой текст.
Вот пример более раннего сценария Julia, который я создал для создания файлов PNG разных размеров для разработки под iOS.
#!/usr/bin/env julia
if isempty(ARGS)
println("Usage: svg2icons.jl pixelsize svgfiles")
println("Example: svg2icons.jl 25 foo.svg bar.svg")
exit()
end
ssize = ARGS[1]
if !all(isdigit, ssize)
println("Error: First argument '$ssize' needs to be pixel width and height.")
exit()
end
size = parse(Int, ssize)
for image in ARGS[2:end]
range = rsearch(image, ".svg")
output = image[1:(first(range) - 1)]
run(`svg $image $output.png $size:$size`)
for x in 2:3
dim = size*x
run(`svg $image $output@$(x)x.png $dim:$dim`)
end
end
Джулия показала мне свой подход, например. установка, разработка и публикация пакетов, что вам действительно не нужны инструменты, вызываемые из оболочки. Просто используйте саму Джулию в качестве оболочки и вызывайте там функции напрямую. Тогда вы избежите всего утомительного связующего кода и синтаксического анализа.
Итак, это мой новый способ написания инструментов оболочки. Я просто добавляю множество полезных функций Julia в файл исходного кода, а затем просто загружаю REPL Julia всякий раз, когда хочу использовать любую из этих функций.
Вот пример той же функциональности, представленной в виде функции, а не скрипта. Это почти половина кода, потому что мне не нужно обрабатывать входные данные. Джулия позаботится о том, чтобы в функцию передавались только аргументы правильного типа.
"`svg2icons(size, svgfiles)` create .png files with multiples of `size`"
function svg2icons(size::Integer, svgfiles::Vector{T}) where T<:AbstractString
for image in svgfiles
range = rsearch(image, ".svg")
output = image[1:(first(range) - 1)]
run(`svg $image $output.png $size:$size`)
for x in 2:3
dim = size*x
run(`svg $image $output@$(x)x.png $dim:$dim`)
end
end
end
Я по-прежнему использую обычную оболочку для переключения каталогов, запуска программ, текстового поиска и запуска программ, но для чего-то более сложного я использую Julia REPL.