Факториальная функция работает в Python, возвращает 0 для Julia

В Python я определяю факториальную функцию:

def fact(n):
    if n == 1:
        return n
    else:
        return n * fact(n-1)

print(fact(100))

и следующим образом в Джулии:

function fact(n)
    if n == 1
        n
    else
        n * fact(n-1)
    end
end

println(fact(100))

Программа python возвращает очень большое число для оценки 100 (как и ожидалось). Julia возвращает 0. С меньшим числом (например, 10) оба они работают.

У меня есть два вопроса:

  • Почему Python обрабатывает это нормально, а Julia - нет.
  • Почему Джулия не выдает ошибку и просто печатает 0 вместо этого?

Ответ 1

Никто не отвечает, почему результат в Julia равен 0.

Julia не проверяет целочисленное умножение для переполнения, и поэтому умножение для 64-битных целых чисел выполняется mod 2 ^ 63. См. этот раздел часто задаваемых вопросов

когда вы выписываете умножение для factorial, вы получаете

1*2*3*4*5*6*7*8*9*10*11*12*13*14*15*16*17*18*19*20*21*22*23*24*25*26*27*28*29*30*31*32*33*34*35*36*37*38*39*40*41*42*43*44*45*46*47*48*49*50*51*52*53*54*55*56*57*58*59*60*61*62*63*64*65*66*67*68*69*70*71*72*73*74*75*76*77*78*79*80*81*82*83*84*85*86*87*88*89*90*91*92*93*94*95*96*97*98*99*100

Это также можно записать как простые множители

2^97 * 3^48 * 5^24 * 7^16 * 11^9 * 13^7 * 17^5 * 19^5 * 23^4 * 29^3 * 31^3 * 37^2 * 41^2 * 43^2 * 47^2 * 53^1 * 59^1 * 61^1 * 67^1 * 71^1 * 73^1 * 79^1 * 83^1 * 89^1 * 97^1

Если вы посмотрите на экспонента 2, вы получите 97. Модульная арифметика дает возможность выполнять функцию mod на любом этапе вычисления, и это не повлияет на результат. 2^97 mod 2^63 == 0, который умножается на остальную цепочку, также равен 0.

UPDATE: Я, конечно, ленился делать эти расчеты на бумаге.

d = Dict{Int,Int}()
for i=1:100
   for (k,v) in factor(i)
       d[k] = get(d,k,0) + v
   end
end
for k in sort(collect(keys(d)))
    print("$k^$(d[k])*")
end

Юлия имеет очень удобную функцию factor() в своей стандартной библиотеке.

Ответ 2

У Julia есть отдельные фиксированные значения целочисленных типов, а также тип BigInt. Тип по умолчанию Int64, который, конечно, 64 бит.

Начиная с 100! занимает около 526 бит, он явно переполняет Int64.

Вы можете решить эту проблему, просто выполнив fact(BigInt(100)) (предположив, что у вас есть require d it), или, конечно, вы можете выполнить преобразование в функции fact.


Python когда-то был одним и тем же. Он имел отдельные типы int, который был 16 или 32 или 64 бит в зависимости от вашей машины, и long, который был произвольной длины. Если вы запустили свою программу на Python 1.5, она либо обернется так же, как Julia, либо вызовет исключение. Решение заключалось бы в вызове fact(100L) или для преобразования в long внутри функции fact.

Однако в какой-то момент в серии 2.x Python связал два типа вместе, поэтому любой int, который переполняется автоматически, становится long. И затем, в 3.0, он объединил два типа целиком, поэтому больше нет отдельного long.


Итак, почему Джулия просто переполняется вместо того, чтобы поднимать ошибку?

В FAQ часто объясняется Почему Джулия использует собственную арифметику целочисленных машин. Который включает поведение обхода при переполнении.


В соответствии с "нативной машинной арифметикой" люди обычно означают "что C делает почти на всех машинах с двумя дополнениями". Особенно на таких языках, как Julia и Python, которые были первоначально построены на вершине C и застряли довольно близко к металлу. В случае с Julia это не просто "дефолт", а намеренный выбор.

В C (по крайней мере, так было в то время), на самом деле до реализации происходит то, что происходит, если вы переполняете целочисленный тип со знаком вроде Int64... но практически на любой платформе, которая изначально использует 2 арифметики дополнения (которая почти на любой платформе, которую вы увидите сегодня), происходит то же самое: она просто обрезает все выше 64-битных, что означает, что вы обмениваете их с положительного на отрицательный. На самом деле, беззнаковые целые типы должны работать таким образом в C. (C, тем временем, работает таким образом, потому что это работает с большинством процессоров.)

В C (в отличие от большинства машинных языков процессоров) нет способа обнаружить, что после факта вы получили переполнение. Итак, если вы хотите поднять OverflowError, вам нужно написать некоторую логику, которая обнаруживает, что умножение будет переполняться, прежде чем делать это. И вы должны использовать эту логику для каждого умножения. Вы можете оптимизировать это для некоторых платформ, написав встроенный ассемблерный код. Или вы можете использовать более крупный тип, но (a), который имеет тенденцию делать ваш код медленнее, и (b) он не работает, если вы уже используете самый большой тип (который Int64 находится на многих платформах сегодня).

В Python, делая каждое умножение до 4x медленнее (обычно меньше, но оно может быть таким высоким), не имеет большого значения, потому что Python тратит больше времени на выбор байт-кода и unboxing целых объектов, чем умножение в любом случае. Но Джулия должна быть быстрее, чем это.

Как объясняет Джон Майлс Уайт в разделе Компьютеры - это машины:

Во многих отношениях Джулия отличает себя от других новых языков своей попыткой восстановить часть власти, которая была потеряна при переходе от C к таким языкам, как Python. Но переход идет с существенной кривой обучения.


Но есть и другая причина для этого: переполненная подписанная арифметика во многих случаях актуальна. Не так много, как переполнение беззнаковой арифметики (именно поэтому C определил арифметику без знака, чтобы работать так, как до первой спецификации ANSI), но есть варианты использования.

И даже если вы, вероятно, хотите, чтобы преобразования типов чаще, чем вы хотите опрокидывания, намного проще выполнять преобразования типов вручную, чем опрокидывание. Если вы когда-либо делали это в Python, выбор операнда для % и правильное распознавание знаков, конечно же, легко ошибаться; отбрасывание на BigInt довольно сложно испортить.


И, наконец, на строго типизированном языке, как и Python, так и Julia, важна стабильность типа. Одной из причин существования Python 3 является то, что старый тип str, магически преобразованный в unicode, вызвал проблемы. Это гораздо менее распространено для вашего типа int, магически преобразующегося в long, чтобы вызвать проблемы, но это может произойти (например, когда вы захватываете значение с провода или через API C и ожидаете записать результат в том же формате). Команда Python dev аргументировала это, делая унификацию int/long, цитируя "практичность превосходит чистоту" и различные другие биты Zen, и в конечном итоге решила, что старое поведение вызвало больше проблем, чем новое поведение. Джулия разработала противоположное решение.

Ответ 3

Я предполагаю, что ответ на этот вопрос заключается в использовании BigInt:

function fact(n::BigInt)                                                                                                                                      
    if n == BigInt(1)                                                                                                                                         
        n                                                                                                                                             
    else                                                                                                                                              
        n * fact(n-BigInt(1))                                                                                                                             
    end                                                                                                                                               
end                                                                                                                                                   

println(fact(BigInt(100))) 

Что дает результат:

93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Протестировано: http://forio.com/julia/repl/

Как указано в некоторых других ответах, Python неявно преобразует int (s), которые превышают максимальный размер для bigint (s) для вас, и поэтому вы получаете результат, который вы ожидаете, вместо того, чтобы терпеть неудачу.

Джулия, с другой стороны, кажется более откровенной в этом вопросе и предпочитает работу над "ожидаемым поведением". Джулия - динамический язык с аннотациями и выводами типа OPtional.

Ответ 4

Python автоматически использует BigInt, который может содержать произвольно большие числа. В Джулии вы должны сделать это сами. Я бы подумал, что это исправлено вот так.

function fact(n::BigInt)                                                                                                                                      
    if n == 1                                                                                                                                         
        n                                                                                                                                             
    else                                                                                                                                              
        n * fact(n-1)                                                                                                                                 
    end                                                                                                                                               
end                                                                                                                                                   

println(fact(BigInt(100))) 

Ответ 5

Одна из причин, по которой Джулия быстро, заключается в том, что они избегают функций, которые могут поставить под угрозу производительность. Это одна из них. В Python интерпретируемый постоянно проверяет, должен ли он автоматически переключаться на библиотеку BigInt. Эта постоянная проверка происходит за счет.

Вот функция, которая делает то, что вы хотите:

function fact(n)
    if n == 0
        1
    else
        big(n) * fact(n-1)
    end
end

println( fact(100) )
println( fact(0) )

Я взял на себя смелость исправлять ошибку в вашей программе: Zero factorial определен и равен 1. Кстати, вы могли бы написать свою функцию следующим образом:

function !(n)
    if n == 0
        1
    else
        big(n) * !(n-1)
    end
end

println( !(100) )
println( !(0) )

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

fact(n) = n == 0 ? 1 : big(n) * !(n-1)

println( fact(100) )
println( fact(0) )

Ответ 6

Кстати, я думаю, что идиоматическим способом сделать это в Julia было бы использование системы типов для компиляции разных версий для разных типов:

fact(n) = n <= zero(n) ? one(n) : n*fact(n-one(n)) 
# one(n) gives you a one, as it were, of the same type as n

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

julia> fact(10)
3628800

julia> fact(100)
0

julia> fact(BigInt(100))
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

Накладные расходы арифметики BigInt vs machine (Int64), хорошо описанные @abarnert, можно увидеть, когда мы смотрим на (LLVM) скомпилированные версии Int64 и BigInt fact():

julia> code_llvm(fact,(Int64,))

define i64 @"julia_fact;23421"(i64) {
top:
  %1 = icmp sgt i64 %0, 0, !dbg !10800
  br i1 %1, label %L, label %if, !dbg !10800

if:                                               ; preds = %top
  ret i64 1, !dbg !10800

L:                                                ; preds = %top
  %2 = add i64 %0, -1, !dbg !10800
  %3 = call i64 @"julia_fact;23398"(i64 %2), !dbg !10800
  %4 = mul i64 %3, %0, !dbg !10800
  ret i64 %4, !dbg !10800
}



julia> code_llvm(fact,(BigInt,))

define %jl_value_t* @"julia_fact;23422"(%jl_value_t*, %jl_value_t**, i32) {
top:
  %3 = alloca [6 x %jl_value_t*], align 8
  %.sub = getelementptr inbounds [6 x %jl_value_t*]* %3, i64 0, i64 0
  %4 = getelementptr [6 x %jl_value_t*]* %3, i64 0, i64 2, !dbg !10803
  store %jl_value_t* inttoptr (i64 8 to %jl_value_t*), %jl_value_t** %.sub, align 8
  %5 = load %jl_value_t*** @jl_pgcstack, align 8, !dbg !10803
  %6 = getelementptr [6 x %jl_value_t*]* %3, i64 0, i64 1, !dbg !10803
  %.c = bitcast %jl_value_t** %5 to %jl_value_t*, !dbg !10803
  store %jl_value_t* %.c, %jl_value_t** %6, align 8, !dbg !10803
  store %jl_value_t** %.sub, %jl_value_t*** @jl_pgcstack, align 8, !dbg !10803
  store %jl_value_t* null, %jl_value_t** %4, align 8, !dbg !10803
  %7 = getelementptr [6 x %jl_value_t*]* %3, i64 0, i64 3
  store %jl_value_t* null, %jl_value_t** %7, align 8
  %8 = getelementptr [6 x %jl_value_t*]* %3, i64 0, i64 4
  store %jl_value_t* null, %jl_value_t** %8, align 8
  %9 = getelementptr [6 x %jl_value_t*]* %3, i64 0, i64 5
  store %jl_value_t* null, %jl_value_t** %9, align 8
  %10 = load %jl_value_t** %1, align 8, !dbg !10803
  %11 = call %jl_value_t* @julia_BigInt2(i64 0), !dbg !10804
  store %jl_value_t* %11, %jl_value_t** %4, align 8, !dbg !10804
  %12 = getelementptr inbounds %jl_value_t* %10, i64 1, i32 0, !dbg !10804
  %13 = getelementptr inbounds %jl_value_t* %11, i64 1, i32 0, !dbg !10804
  %14 = call i32 inttoptr (i64 4535902144 to i32 (%jl_value_t**, %jl_value_t**)*)(%jl_value_t** %12, %jl_value_t** %13), !dbg !10804
  %15 = icmp sgt i32 %14, 0, !dbg !10804
  br i1 %15, label %L, label %if, !dbg !10804

if:                                               ; preds = %top
  %16 = call %jl_value_t* @julia_BigInt2(i64 1), !dbg !10804
  %17 = load %jl_value_t** %6, align 8, !dbg !10804
  %18 = getelementptr inbounds %jl_value_t* %17, i64 0, i32 0, !dbg !10804
  store %jl_value_t** %18, %jl_value_t*** @jl_pgcstack, align 8, !dbg !10804
  ret %jl_value_t* %16, !dbg !10804

L:                                                ; preds = %top
  store %jl_value_t* %10, %jl_value_t** %7, align 8, !dbg !10804
  store %jl_value_t* %10, %jl_value_t** %8, align 8, !dbg !10804
  %19 = call %jl_value_t* @julia_BigInt2(i64 1), !dbg !10804
  store %jl_value_t* %19, %jl_value_t** %9, align 8, !dbg !10804
  %20 = call %jl_value_t* @"julia_-;23402"(%jl_value_t* inttoptr (i64 140544121125120 to %jl_value_t*), %jl_value_t** %8, i32 2), !dbg !10804
  store %jl_value_t* %20, %jl_value_t** %8, align 8, !dbg !10804
  %21 = call %jl_value_t* @"julia_fact;23400"(%jl_value_t* inttoptr (i64 140544559367232 to %jl_value_t*), %jl_value_t** %8, i32 1), !dbg !10804
  store %jl_value_t* %21, %jl_value_t** %8, align 8, !dbg !10804
  %22 = call %jl_value_t* @"julia_*;23401"(%jl_value_t* inttoptr (i64 140544121124768 to %jl_value_t*), %jl_value_t** %7, i32 2), !dbg !10804
  %23 = load %jl_value_t** %6, align 8, !dbg !10804
  %24 = getelementptr inbounds %jl_value_t* %23, i64 0, i32 0, !dbg !10804
  store %jl_value_t** %24, %jl_value_t*** @jl_pgcstack, align 8, !dbg !10804
  ret %jl_value_t* %22, !dbg !10804
}