From e5c5358cc9e914fdc26e153cf08efce3d86d4501 Mon Sep 17 00:00:00 2001 From: Thomas Breuer Date: Tue, 24 Sep 2024 09:31:09 +0200 Subject: [PATCH] Support keyword arguments in `CallJuliaFunctionWithCatch`, and a few other improvements (#1043) - support keyword arguments in `CallJuliaFunctionWithCatch` - do not use the deprecated and undocumented `Core._apply` - start better documentation of Julia syntax features --- pkg/JuliaInterface/gap/JuliaInterface.gd | 63 +++++++++++++++++++----- pkg/JuliaInterface/gap/calls.gi | 17 +++++-- pkg/JuliaInterface/tst/utils.tst | 2 + src/packages.jl | 20 ++++---- src/utils.jl | 42 ++++++++++++---- 5 files changed, 108 insertions(+), 36 deletions(-) diff --git a/pkg/JuliaInterface/gap/JuliaInterface.gd b/pkg/JuliaInterface/gap/JuliaInterface.gd index 95c47589..d2eda1ee 100644 --- a/pkg/JuliaInterface/gap/JuliaInterface.gd +++ b/pkg/JuliaInterface/gap/JuliaInterface.gd @@ -224,6 +224,24 @@ DeclareGlobalFunction( "JuliaImportPackage" ); #! @Section Access to &Julia; objects +#! Not all &Julia; syntax features are supported in ⪆. +#! For important ones, the interface provides ⪆ functions or +#! helper functions written in &Julia; to use them in ⪆. +#! For example, allows one to use +#! &Julia;'s try/catch statements. +#! +#! Here is a selection of other workarounds for &Julia; syntax features. +#! +#! +#! &Julia;'s RefValue objects can be handled as follows. +#! If x is such an object then its value can be fetched with +#! Julia.GAP.getindex( x ), +#! a value v of the right type can be set with +#! Julia.GAP.setindex( x, v ), +#! and one can check with Julia.GAP.isassigned( x ) +#! whether x has a value. +#! +#! #! @Description #! This global variable represents the &Julia; module Main, @@ -242,6 +260,19 @@ DeclareGlobalFunction( "JuliaImportPackage" ); #! gap> Julia.Main.x; #! 1 #! @EndExampleSession +#! +#! Note that not all &Julia; variables are directly visible in its +#! Main module. +#! For example, &Julia; variables from the interface to ⪆ are defined +#! in the &Julia; module GAP or its submodules. +#! It is safe to access this module as Julia.GAP. +#! +#! @BeginExampleSession +#! gap> Julia.GAP; +#! +#! gap> Julia.GAP.prompt; +#! +#! @EndExampleSession DeclareGlobalVariable( "Julia" ); #! @Arguments juliaobj @@ -259,11 +290,13 @@ DeclareGlobalVariable( "Julia" ); #! @EndExampleSession DeclareGlobalFunction( "JuliaTypeInfo" ); -#! @Arguments juliafunc, arguments +#! @Arguments juliafunc, arguments[, kwargs] #! @Returns a record. #! @Description #! The function calls the &Julia; function juliafunc -#! with arguments in the ⪆ list arguments, +#! with ordinary arguments in the ⪆ list arguments +#! and optionally with keyword arguments given by the component names (keys) +#! and values of the ⪆ record kwargs, #! and returns a record with the components ok and value. #! If no error occurred then ok has the value true, #! and value is the value returned by juliafunc. @@ -286,20 +319,29 @@ DeclareGlobalFunction( "JuliaTypeInfo" ); #! false #! gap> res.value{ [ 1 .. Position( res.value, '(' )-1 ] }; #! "LinearAlgebra.SingularException" +#! gap> fun:= Julia.range;; +#! gap> CallJuliaFunctionWithCatch( fun, [ 2, 10 ], rec( step:= 2 ) ); +#! rec( ok := true, value := ) +#! gap> res:= CallJuliaFunctionWithCatch( fun, [ 2, 10 ], +#! > rec( step:= GAPToJulia( "a" ) ) );; +#! gap> res.ok; +#! false +#! gap> res.value{ [ 1 .. Position( res.value, '(' )-1 ] }; +#! "MethodError" #! @EndExampleSession DeclareGlobalFunction( "CallJuliaFunctionWithCatch" ); -#! @Arguments juliafunc, arguments, arec +#! @Arguments juliafunc, arguments, kwargs #! @Returns the result of the &Julia; function call. #! @Description #! The function calls the &Julia; function juliafunc #! with ordinary arguments in the ⪆ list arguments #! and keyword arguments given by the component names (keys) and values -#! of the record arec, +#! of the record kwargs, #! and returns the function value. #! #! Note that the entries of arguments and the components of -#! arec are not implicitly converted to &Julia;. +#! kwargs are not implicitly converted to &Julia;. #! @BeginExampleSession #! gap> CallJuliaFunctionWithKeywordArguments( Julia.Base.round, #! > [ GAPToJulia( Float( 1/3 ) ) ], rec( digits:= 5 ) ); @@ -388,8 +430,8 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" ); #! #! #! , -#! delegating to Julia.Core._apply -#! (this yields the function call syntax in ⪆, +#! delegating to &Julia;'s func(args...) syntax; +#! this yields the function call syntax in ⪆, #! it is installed also for objects in #! , #! @@ -397,10 +439,8 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" ); #! access to and assignment of entries of arrays, via #! , #! , -#! -#! and the (up to ⪆ 4.11 undocumented) operations MatElm and -#! SetMatElm, +#! , and +#! , #! delegating to #! Julia.Base.getindex and #! Julia.Base.setindex, @@ -459,7 +499,6 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" ); #! gap> m + m; #! #! @EndExampleSession -#TODO: add the cross-references to MatElm, SetMatElm when they are documented #! @InsertChunk JuliaHelpInGAP diff --git a/pkg/JuliaInterface/gap/calls.gi b/pkg/JuliaInterface/gap/calls.gi index e01b0ff4..45f25c1a 100644 --- a/pkg/JuliaInterface/gap/calls.gi +++ b/pkg/JuliaInterface/gap/calls.gi @@ -12,7 +12,7 @@ InstallMethod( CallFuncList, [ "IsJuliaObject", "IsList" ], function( julia_obj, args ) args := GAPToJulia( _JL_Vector_Any, args, false ); - return Julia.Core._apply( julia_obj, args ); + return Julia.GAP._apply( julia_obj, args ); end ); InstallMethod( CallFuncList, @@ -22,14 +22,21 @@ InstallMethod( CallFuncList, end ); InstallGlobalFunction( CallJuliaFunctionWithCatch, - function( julia_obj, args ) + function( julia_obj, args, kwargs... ) local res; args := GAPToJulia( _JL_Vector_Any, args, false ); if IsFunction( julia_obj ) then julia_obj:= Julia.GAP.UnwrapJuliaFunc( julia_obj ); fi; - res:= Julia.GAP.call_with_catch( julia_obj, args ); + if Length( kwargs ) = 0 then + res:= Julia.GAP.call_with_catch( julia_obj, args ); + elif Length( kwargs ) = 1 and IsRecord( kwargs[1] ) then + kwargs := GAPToJulia( _JL_Dict_Any, kwargs[1], false ); + res:= Julia.GAP.call_with_catch( julia_obj, args, kwargs ); + else + Error( "usage: CallJuliaFunctionWithCatch( , [, ]" ); + fi; if res[1] then return rec( ok:= true, value:= res[2] ); else @@ -38,7 +45,7 @@ InstallGlobalFunction( CallJuliaFunctionWithCatch, end ); InstallGlobalFunction( CallJuliaFunctionWithKeywordArguments, - { julia_obj, args, arec } -> Julia.GAP.kwarg_wrapper( julia_obj, + { julia_obj, args, kwargs } -> Julia.GAP.kwarg_wrapper( julia_obj, # non-recursive conversions GAPToJulia( _JL_Vector_Any, args, false ), - GAPToJulia( _JL_Dict_Any, arec, false ) ) ); + GAPToJulia( _JL_Dict_Any, kwargs, false ) ) ); diff --git a/pkg/JuliaInterface/tst/utils.tst b/pkg/JuliaInterface/tst/utils.tst index 734a06ca..612a02b7 100644 --- a/pkg/JuliaInterface/tst/utils.tst +++ b/pkg/JuliaInterface/tst/utils.tst @@ -19,6 +19,8 @@ gap> res.ok; false gap> StartsWith( res.value, "DomainError" ); true +gap> CallJuliaFunctionWithCatch( Julia.Base.sqrt, [ 4 ], rec() ); +rec( ok := true, value := ) ## gap> JuliaEvalString(fail); diff --git a/src/packages.jl b/src/packages.jl index 615a6d2c..10dcadc9 100644 --- a/src/packages.jl +++ b/src/packages.jl @@ -10,8 +10,8 @@ const DOWNLOAD_HELPER = Ref{Downloads.Downloader}() function init_packagemanager() #TODO: -# As soon as PackageManager uses utils' Download function, -# we need not replace code from PackageManager anymore. +# As soon as GAP.jl can rely on a good enough version of PackageManager +# we need not replace `PKGMAN_DownloadURL` anymore. # (And the function should be renamed.) res = load("PackageManager") @assert res @@ -59,15 +59,15 @@ function init_packagemanager() # put the new method in the first position meths = Globals.Download_Methods Wrappers.Add(meths, GapObj(r, recursive=true), 1) - - # monkey patch PackageManager so that we can disable removal of - # package directories for debugging purposes - orig_PKGMAN_RemoveDir = Globals.PKGMAN_RemoveDir - replace_global!(:PKGMAN_RemoveDir, function(dir) - Globals.ValueOption(GapObj("debug")) == true && return - orig_PKGMAN_RemoveDir(dir) - end) end + + # monkey patch PackageManager so that we can disable removal of + # package directories for debugging purposes + orig_PKGMAN_RemoveDir = Globals.PKGMAN_RemoveDir + replace_global!(:PKGMAN_RemoveDir, function(dir) + Globals.ValueOption(GapObj("debug")) == true && return + orig_PKGMAN_RemoveDir(dir) + end) end """ diff --git a/src/utils.jl b/src/utils.jl index be9769cd..33860d9a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -15,6 +15,9 @@ function _setglobal(M::Module, name::Symbol, val::Any) end end +# avoid the deprecated `Core._apply` +_apply(func, args) = func(args...) + """ get_symbols_in_module(m::Module) :: Vector{Symbol} @@ -30,20 +33,24 @@ function get_symbols_in_module(m::Module) end """ - call_with_catch(juliafunc, arguments) + call_with_catch(func, args::Vector) + call_with_catch(func, args::Vector, kwargs::Dict{Symbol,T}) where T Return a tuple `(ok, val)` -where `ok` is either `true`, meaning that calling the function `juliafunc` -with `arguments` returns the value `val`, +where `ok` is either `true`, meaning that calling `func` +with arguments `args` (and optionally with keyword arguments given by +the keys and values of `kwargs`) returns the value `val`, or `false`, meaning that the function call runs into an error; in the latter case, `val` is set to the string of the error message. +This function is used on the GAP side. + # Examples ```jldoctest -julia> GAP.call_with_catch(sqrt, 2) +julia> GAP.call_with_catch(sqrt, [2]) (true, 1.4142135623730951) -julia> flag, res = GAP.call_with_catch(sqrt, -2); +julia> flag, res = GAP.call_with_catch(sqrt, [-2]); julia> flag false @@ -51,11 +58,30 @@ false julia> startswith(res, "DomainError") true +julia> GAP.call_with_catch(range, [2, 10], Dict(:step => 2)) +(true, 2:2:10) + +julia> flag, res = GAP.call_with_catch(range, [2, 10], Dict(:step => "a")); + +julia> flag +false + +julia> startswith(res, "MethodError") +true ``` """ -function call_with_catch(juliafunc, arguments) +function call_with_catch(func, args) try - res = Core._apply(juliafunc, arguments) + res = func(args...) + return (true, res) + catch e + return (false, string(e)) + end +end + +function call_with_catch(func, args::Vector, kwargs::Dict{Symbol,T}) where T + try + res = func(args...; [k => kwargs[k] for k in keys(kwargs)]...) return (true, res) catch e return (false, string(e)) @@ -70,7 +96,6 @@ given by the keys and values of `kwargs`. This function is used on the GAP side, in calls of Julia functions that require keyword arguments. -Note that `jl_call` and `Core._apply` do not support keyword arguments. # Examples ```jldoctest @@ -79,7 +104,6 @@ julia> range(2, length = 5, step = 2) julia> GAP.kwarg_wrapper(range, [2], Dict(:length => 5, :step => 2)) 2:2:10 - ``` """ function kwarg_wrapper(func, args::Vector{T1}, kwargs::Dict{Symbol,T2}) where {T1,T2}