Взгляд на использование самоанализа для работы с типами в Джулии

Введение

Когда дело доходит до окончательного выполнения вашего кода, многие из языков программирования высокого уровня, которые мы часто используем, не предлагают множество инструментов, позволяющих заглянуть в язык во время выполнения. Конечно, мы могли бы напечатать значения, у нас может быть некоторый объем вывода, который говорит нам, работает код или нет, но гораздо сложнее работать с определенными типами и именами во время выполнения. Я имею в виду, что это имеет смысл, это не то, что обычно нужно делать во время выполнения, анализировать типы и определенные поля повсюду — это просто не оправдано в большинстве случаев.

Однако, хотя в большинстве случаев может оказаться, что самоанализ вам не нужен, он необходим компилятору вашего языка, чтобы лучше понять, что происходит, а также ваше окружение. При этом есть также несколько случаев, когда можно работать вместе с компилятором или, возможно, управлять областями действия так же, как это делает компилятор. Более того, интроспекция может иметь очень действенный эффект при метапрограммировании в Джулии. Хотя информация об этом доступна в документации по Julia, она находится в документации для разработчиков для Julia, а не в обычной документации. При этом я предполагаю, что большинство людей, использующих Julia, не собираются разрабатывать сам язык, но, безусловно, могли бы использовать самоанализ на каком-то этапе использования языка, поскольку я считаю ценным то, что язык может работать с большим количеством различных приложений. При этом, если вы предпочитаете читать документацию, а не мое объяснение этих функций, вот ссылка на документацию по Джулии:



Кроме того, код, который я использовал для этого небольшого обзора, доступен в формате блокнота для тех, кто может захотеть запустить эти примеры самостоятельно:



Типы данных

Первое, что нам нужно понять о типах данных в целом, это то, что они имеют свойства, называемые «полями» внутри Julia. Мы можем проверять поля любого заданного типа или типа данных. Конечно, у полей есть несколько различных свойств, но два самых важных — это имена полей и их типы. Мы можем получить имена полей, используя метод с тем же именем,

mutable struct Noodle
    class::Symbol
    length::Integer
end
fieldnames(Noodle)
(:class, :length)

Это возвращает кортеж символов, которые представляют каждое имя поля внутри этого типа. В Julia все, что вы определяете, имеет соответствующий символ. При этом есть также символы, соответствующие каждому значению, созданному внутри вашей глобальной среды. Мы также можем получать и устанавливать поля с помощью getfield() и setfield!() соответственно, однако во многих случаях setfield!() не работает. Всякий раз, когда мы обращаемся к полю типа, используя ., например.

Noodle.class

на самом деле мы вызываем метод getfield. Если мы используем

Noodle.class = :spaghet

Затем мы вызываем метод setfield. Мы можем получить поля из модулей и типов. Например, мы можем получить один из main:

getfield(Main, :Noodle)
Noodle

Main — это просто имя глобального модуля верхнего уровня в Julia.

Наконец, важным компонентом любого аргумента или данных в Julia будут его типы. Мы можем получить к нему доступ, используя как метод fieldtypes(), так и просто поле .types, которое добавляется к каждому типу в Julia.

fieldtypes(Noodle)
(Symbol, Integer)
Noodle.types

svec(Symbol, Integer)

Подтипы

Хотя они менее ориентированы на время выполнения, их все же полезно знать, когда дело доходит до интроспекции компонентов иерархии типов данного модуля или чего-то еще. Мы можем определить, является ли тип подтипом чего-либо, используя оператор подтипа <:. Этот оператор используется как для обозначения того, что тип является подтипом абстрактного типа при использовании в его определении, так и для возврата логического значения относительно того, является ли предоставленный тип подтипом абстрактного типа. Во-первых, давайте посмотрим на настройку подтипа:

abstract type Pasta end
struct Spaghet <: Pasta
    length::Integer
end

Этот новый тип Spaghet унаследует все методы, применяемые к типу Pasta. Теперь мы также можем использовать тот же точный синтаксис, чтобы узнать, действительно ли Spaghet является подтипом Pasta, что будет правдой:

Spaghet <: Pasta
true

Мы также можем получить все подтипы для данного абстрактного типа, используя метод subtypes():

subtypes(Pasta)
1-element Vector{Any}:
 Spaghet

Если вы не посвящены в мир иерархий юлианского типа, у меня есть целая статья, которая более подробно описана здесь:



Кроме того, вот функция дерева подтипов, которая также может быть очень полезной для лучшей визуализации подтипов. Я только что нашел это по какой-то проблеме здесь, но я думаю, что это определенно неплохо для тех, кто хочет лучше понять определенный тип:

function subtypetree(t, level=1, indent=4)
           level == 1 && println(t)
           for s in subtypes(t)
             println(join(fill(" ", level * indent)) * string(s))
             subtypetree(s, level+1, indent)
           end
       end
subtypetree(Number)
Number
    Complex
    Real
        AbstractFloat
            BigFloat
            Float16
            Float32
            Float64
        AbstractIrrational
            Irrational
        Integer
            Bool
            Signed
                BigInt
                Int128
                Int16
                Int32
                Int64
                Int8
            Unsigned
                UInt128
                UInt16
                UInt32
                UInt64
                UInt8
        Rational

Под капотом

Последние несколько вещей, которые я хотел показать, предназначены для более глубокого самоанализа. В нашем первом примере мы рассмотрим макрос @code_llvm, который просто дает нам код llvm для данного выражения:

@code_llvm subtypetree(Number)
;  @ In[16]:1 within `subtypetree`
define nonnull {}* @japi1_subtypetree_2021({}* %0, {}** %1, i32 %2) #0 {
top:
  %3 = alloca {}**, align 8
  store volatile {}** %1, {}*** %3, align 8
  %4 = load {}*, {}** %1, align 8
;  @ In[16]:2 within `subtypetree`
  call void @j_subtypetree_2022({}* nonnull %4, i64 signext 1, i64 signext 4) #1
  ret {}* inttoptr (i64 139868392615944 to {}*)
}

Еще один похожий небольшой макрос — @code_typed. Это похоже на использование Meta.@lower, и в большинстве случаев я думаю, что это, вероятно, лучший выбор, если только мы не хотим получить информацию о вызове одного метода, например, это не будет работать:

@code_typed debuginfo = :source begin
    +(1, 1, 5)
    -(7, 3)
end
expression is not a function call, or is too complex for @code_typed to analyze; break it down to simpler parts if possible. In some cases, you may want to use Meta.@lower.

По сравнению с Meta.@lower:

Meta.@lower begin
    +(1, 1, 5)
    -(7, 3)
end
:($(Expr(:thunk, CodeInfo(
    @ In[24]:2 within `top-level scope`
1 ─      1 + 1 + 5
│   @ In[24]:3 within `top-level scope`
│   %2 = 7 - 3
└──      return %2
))))

Последнее, чем я хочу поделиться в отношении самоанализа, — это метод varinfo(). Этот метод великолепен, потому что он может пролить свет на использование памяти разных типов, а также дать нам хорошее описание того, что находится в нашей среде.

varinfo()

Заключение

Самоанализ является важным аспектом языка программирования. Это действительно может пригодиться, когда мы работаем с типами, где мы можем не знать полей и т. д. Я думаю, что это тем более важно, когда мы используем язык с динамической типизацией. Кроме того, для языков более высокого уровня, которые не позволяют нам отслеживать или контролировать такие вещи, как память и другие аппаратные компоненты, во многих случаях действительно имеет смысл использовать интроспекцию в наших интересах, насколько это возможно. Я думаю, что юлианские методы интроспекции в большинстве случаев вполне уместны, вещь, которую я, вероятно, использую чаще всего с точки зрения интроспекции, — это метод varinfo(). Очень здорово иметь представление о том, сколько памяти занимают вещи, особенно когда речь идет о разработке собственного пакета для чего-либо. Спасибо, что прочитали мою статью, я очень ценю ее, и я надеюсь, что этот обзор был полезен для освещения некоторых лучших способов управления языком Julia!