Разъяснение разработки на основе 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.