Question The powers hell ou la puissance de l'enfer

Plus d'informations
il y a 11 ans 10 mois #2914 par Laurent Dardenne
Arnaud écrit:

Pourrais tu nous dire à quoi peuvent servir les closures ?

Voilà ce que je peux en dire aujourd'hui
L'usage d'une closure est plus courant dans les langages orienté objet et est liée aux méthodes anonyme, sous PowerShell un scriptblock, on peut les voir comme \"une manière de programmer\".
Souvent leur interêt permet de ne pas créer de classse qui ne porterait qu'une seule fonction.
Bon on peut s'en passer et programmer de manière \"classique\".

Le premier interêt que j'ai trouvé à ce code était justement une autre manière de faire et elle présente de nombreux mécanisme de PS et .NET.
Pour un curieux comme moi, c'est du pain béni :-)

L'interêt que je peux présentement y trouver est de paramètrer des traitements( function contenant la closure) puis de passer ce code en paramètre à une autre fonction.
Cela rejoint le principe des délégués (pointeur de fonction), la différence, très sommaire, est qu'on y mémorise les variables locales.
Ici on nomme cette technique le currying , désolé d'introduire ce terme pour expliquer le premier...
Encore une fois c'est un mécanisme plus orienté développeur qu'administrateur.

Reprenons l'exemple cité:
[code:1]
function NewCounter
{
$i = 0
return closure {
$i++;
return $i
};
}
$counter = NewCounter
$compteur=&$counter
[/code:1]
On peut le transformer ainsi :
[code:1]
function NewCounter2($Compteur)
{
$Compteur++
return $Compteur++
};

$compteur=0
$compteur=NewCounter2 $Compteur
$compteur
[/code:1]
L'initialisation se fait à l'extérieur de la fonction et on doit lui passer en paramètre la variable $compteur qu'on souhaite incrémenter.

Reprenons le second exemple cité:
[code:1]
function NewDecorator
{
param ([string]$decoration)
return closure {
param ([string]$text)
return $decoration + $text + $decoration;
};
}

$sharpDecorator = NewDecorator '#';
$SlashDecorator = NewDecorator '//';
[/code:1]
On peut réécrire cette fonction auquel on passe le texte à commenter et la chaîne de commentaire :
[code:1]
function Commenter1($Texte,$decoration)
{
foreach($Ligne in $Texte)
{$decoration + $Ligne + $decoration }

}
$A=dir|%{$_.name}

Commenter1 $A \"#\"
Commenter1 $A \"//\"
[/code:1]
Ici comme il n'y a qu'un paramètre cela ne change pas grand chose mais dans le cas où le traitement en nécessite plusieurs cela peut faciliter l'écriture.

La closure permet aussi de spécialiser le traitement, ici commenter des lignes de PS ou du C#, on a donc un décorateur pour chaque langage.
Dans le cas où j'ai besoin d'un décorateur pour le C, j'ai besoin de 2 paramètres :
[code:1]
function Commenter2($Texte,[string]$Begin, [string]$End)
{
foreach($Ligne in $Texte)
{$Begin + $text + $End}
}

Commenter2 $A \"/*\" \"*/\"
[/code:1]
PS ne propose pas la surchage de méthode/fonction, i.e. un même nom de fonction mais avec un nombre de paramètre différent. Mais on peut le simuler.
Donc dans ce cas si je souhaite utiliser le même code (commenter1) ce n'est pas possible, pourtant commenter2 à le même objectif mais elle ne le réalise pas tout à fait de la même manière.

On peut réécrire cette fonction auquel on passe la closure et son paramètre :
[code:1]
function Commenter($Texte,$Sb)
{
foreach($Ligne in $Texte)
{&$Sb $Ligne }

}
[/code:1]
Avec les closures je crée un nouveau décorateur et c'est la closure qui mémorise les paramètres du décorateur, de plus la closure recoit tjr un seul paramètre, le texte sur lequel on effectue le traitement.
[code:1]
function CDecorator
{
param ([string]$Begin, [string]$End)
return closure {
param ([string]$text)
return $Begin + $text + $End;
};
}

$CDecorator=CDecorator \"/*\" \"*/\"
[/code:1]
Ainsi je peux appeler les 3 décorateurs sur le tableaux $A sans me préoccuper du nombre de paramètres des décorateurs et ma fonction commenter ne nécessite pas de maintenance, c'est à dire de modifications :
[code:1]
Commenter $A $sharpDecorator
Commenter $A $SlashDecorator
Commenter $A $CDecorator
[/code:1]
Donc sans closure le nombre de paramètre doit être propagé, on pourrait les propager dans un seul paramètre de type tableau, voir créer un objet synthétique;
Avec une closure le nombre de paramètre est réduit, on ne les propage pas car ils sont mémorisés automatiquement,et le code n'est ni à modifier ni à adapter.
Reste à savoir si mes explications sur le sujet sont suffisante, je ne le pense pas.

Tout compte fait l'usage de délégué est plus approprié
blogs.msdn.com/powershell/archive/2006/07/25/678259.aspx
[code:1]
[string[]]$T=[System.IO.FileMode]|gm -static -membertype property|% {$_.Name}
$Deleg=get-delegate \"System.Predicate``1[System.String]\" {write-host \"Test\"}
#[Array]::Find($T, $Deleg)
Invoke-GenericMethod ([System.Array]) Find String ($T,$Deleg)
[/code:1]
Malheureusement ce code ne fonctionne pas, le script Invoke-GenericMethod ne semble pas prendre en compte tous les cas de figure.

Ou encore avec un script bloc sans closure :
[code:1]
$sbsharpDecorator={ param ([string]$Text)
return \"#{0}#\" -F $Text;
}
$sbSlashDecorator={ param ([string]$Text)
return \"//{0}//\" -F $Text;
}
$sbCDecorator={ param ([string]$Text)
return \"/*{0}*/\" -F $Text;
}

$A=dir|%{$_.name}
Commenter $A $sbsharpDecorator
Commenter $A $sbSlashDecorator
Commenter $A $sbCDecorator
[/code:1]
A noter une duplication de code dans ce cas.
Et effectivement à voir cette approche des closures, de prime abord on pourrait se dire pourquoi faire simple quand on peut faire compliqué ;-)
[edit]
J'ai modifié la syntaxe d'appel du délégué qui était érroné mais cela ne régle pas le pb...<br><br>Message édité par: Laurent Dardenne, à: 8/10/08 15:48

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 11 ans 10 mois #2917 par Laurent Dardenne
Un autre oubli, le lien sur le script Invoke-GenericMethod
www.leeholmes.com/blog/InvokingGenericMe...sesInPowerShell.aspx
Je pense que le problème vient de la création du délégué. Ce code fonctionne avec des arguments de type différent.

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 11 ans 9 mois #2947 par Laurent Dardenne
Suite et fin.

Concernant l'usage d'un mot clé pour mémoriser uniquement les variables déclarées dans le code englobant notre closure, j'avais envisagé d'utiliser une fonction nommée Var.
Cette approche fonctionne, bien que nécessitant un code conséquent pour l'analyse des paramètres pour prendre en charge les constructions suivante qui doivent respecter la syntaxe de PowerShell :
[code:1]
function Test
{
Var instance instance3
Var 1 2 3;
#4 paramètres dont un tableau
var 1 2 3 4,5,6 ;
#5 paramètres dont 2 tableaux
var 1 2 3 (4,5,6) a,b
var 1 2 3 @(4,5,6) a,b
#etc
[/code:1]
Un autre aspect à prendre en compte concerne la définition d'une syntaxe de déclaration, la déclaration du type et l'assignation par défaut :
[code:1]
#A la Pascal-Delphi
var 1:[string]
#Ou à la C#
var [System.String] 1
var [System.Truc+NestedType] J= (new-object System.Truc+NestedType parametre_1)
var [String] 1=$null
var [string] x={ &amp;Scriptbloc ...}
var [string] 1=( Get-variable $x)
[/code:1]
Donc les DSL c'est facile et surtout ça peut rapporter gros en développement ;-)

L'ébauche de cette fonction Var mémorise toutes les variables déclarées par les appels successif de la pseudo-clause Var.
Outre le problème d'analyse des paramètres il reste celui de la récupération de cette liste de variables dans le code de la fonction Closure.
Pour ce faire on déclare une variable dans la portée parente de notre fonction Var
[code:1]
if (!(test-path Variable:__ClosureVarStore))
{ #Création de la variable __ClosureVarStore
Set-Variable '__ClosureVarStore' $Variables -Scope 1
}
else
{ #La variable __ClosureVarStore est déjà déclarée.\&quot;
#On récupère la variable de la portée parente
#Et on évite les duplications de nom de variables
$__ClosureVarStore+=$Variables|? {$__ClosureVarStore -notcontains $_}
Set-Variable '__ClosureVarStore' $__ClosureVarStore -Scope 1
}
[/code:1]
Ainsi tout code utilisant la pseudo-clause Var n'a pas besoin de se préocupper de la gestion de cette liste.
On récupére tout naturellement la variable automatique '__ClosureVarStore' dans le code de la closure.
Cette mécanique fonctionne pour des déclarations simples.

Mais si les variables peuvent être ainsi gérées il reste la clause param. Bon ici ça complique, mieux vaut abandonner cette piste :whistle: .

Donc comment faire pour récupérer les variables déclarées dans le code et dans la clause param ?
La solution retenue se base sur la manipulation de la portée des variables.
Les cmdlets Get-Variable et Set-Variable proposent le paramètre Scope qui défini la portée dans laquelle on récupère ou déclare une variable. Il s'agit en quelque sorte d'un mécanisme de création de variable \&quot;automatique\&quot;.

Ainsi dans la fonction Closure on récupère les variables du code englobant :
[code:1]
$private:«»ParentPrivateVariables=Get-Variable -scope 1|? {$_.options -notmatch \&quot;AllScope\&quot;}
[/code:1]
Tout en filtrant celles qui sont globales [code:1]($MaVariable.Options=\&quot;AllScope\&quot;«»)[/code:1]

On modifie donc le code de la fonction Closure comme ceci :
[code:1]
#Recherche des variables de la portée parente
#Au sein du code parent le contenu des portées \&quot;local\&quot; et \&quot;private\&quot; sont identiques en PS v1.0, sauf erreur de ma part.
#On ne récupére que les variables, automatiques ou non, dont la portée est \&quot;none\&quot;
$private:«»ParentPrivateVariables=Get-Variable -scope 1|? {$_.options -notmatch \&quot;AllScope\&quot;}

#Mémorisation de l'environnement de la portée enfant (toutes les variables sauf les variables automatiques)
$autoVariableNames =
@(
'$', '?', '^', '_', 'args', 'ConfirmPreference', 'ConsoleFileName', 'DebugPreference', 'Error', 'ErrorActionPreference',
'ErrorView', 'ExecutionContext', 'false', 'FormatEnumerationLimit', 'foreach', 'HOME', 'Host', 'input', 'LASTEXITCODE',
'lastWord', 'line', 'Matches', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount',
'MaximumHistoryCount', 'MaximumVariableCount', 'MyInvocation', 'NestedPromptLevel','null', 'OutputEncoding', 'PID',
'PROFILE', 'ProgressPreference', 'PSHOME', 'PWD', 'ReportErrorShowExceptionClass', 'ReportErrorShowInnerException',
'ReportErrorShowSource','ReportErrorShowStackTrace', 'ShellId', 'StackTrace', 'switch', 'true', 'VerbosePreference',
'WarningPreference', 'WhatIfPreference',
#PS v2 ctp2
'$CommandLineParameters','$PSVersionTable','$PSCulture','$PSUICulture'
);

#On ne garde que les variables 'non-automatiques', celles déclarées dans le corps de la méthode ou celles de la clause param
$private:environment = $private:«»ParentPrivateVariables| ? { $autoVariableNames -notcontains $_.Name }
[/code:1]
De cette manière on ne mémorise que le strict nécessaire, c'est à dire les variables du code englobant.
D'où un gain conséquent en occupation mémoire.

Tout comptes fait cette étude s'est révélée très instructive :-)

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 11 ans 9 mois #2948 par Laurent Dardenne
Le code modifié et commenté :
[code:1]
function global:closure
{ #Crée un objet Closure

param ([ScriptBlock]$private:«»script)
trap { break; }
#Vérification de l'argument
if ($null -eq $script)
{ throw 'The script is null.' }
Write-debug (\&quot;Script à exécuter : {0}\&quot; -F $private:«»script)

#La variable ClosureStore, de type hash table, maintient la liste de toutes les closures
if ($null -eq $global:ClosureStore)
{
Set-Variable 'ClosureStore' @{} -Scope 'global' -Option 'Constant, AllScope';
}
#Supprime dans la hastable les éléments qui ont été collectés par le GC
($ClosureStore.GetEnumerator() | ? { !$_.Value.IsAlive; }) | ? { $() -ne $_; } | % { $ClosureStore.Remove($_.Key); };


#Recherche des variables de la portée parente
#Au sein du code parent le contenu des portées \&quot;local\&quot; et \&quot;private\&quot; sont identiques en PS v1.0, sauf erreur de ma part.
#On ne récupére que les variables, automatiques ou non, dont la portée est \&quot;none\&quot;
$private:«»ParentPrivateVariables=Get-Variable -scope 1|? {$_.options -notmatch \&quot;AllScope\&quot;}

#Mémorisation de l'environnement de la portée enfant (toutes les variables sauf les variables automatiques)
$autoVariableNames =
@(
'$', '?', '^', '_', 'args', 'ConfirmPreference', 'ConsoleFileName', 'DebugPreference', 'Error', 'ErrorActionPreference',
'ErrorView', 'ExecutionContext', 'false', 'FormatEnumerationLimit', 'foreach', 'HOME', 'Host', 'input', 'LASTEXITCODE',
'lastWord', 'line', 'Matches', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount',
'MaximumHistoryCount', 'MaximumVariableCount', 'MyInvocation', 'NestedPromptLevel','null', 'OutputEncoding', 'PID',
'PROFILE', 'ProgressPreference', 'PSHOME', 'PWD', 'ReportErrorShowExceptionClass', 'ReportErrorShowInnerException',
'ReportErrorShowSource','ReportErrorShowStackTrace', 'ShellId', 'StackTrace', 'switch', 'true', 'VerbosePreference',
'WarningPreference', 'WhatIfPreference',
#PS v2 ctp2
'$CommandLineParameters','$PSVersionTable','$PSCulture','$PSUICulture'
);

#On ne garde que les variables 'non-automatiques', celles déclarées dans le corps de la méthode ou celle de la clause param
$private:environment = $private:«»ParentPrivateVariables| ? { $autoVariableNames -notcontains $_.Name }
#need Dbgview.exe
[int] $private:level=1;[string] $category=\&quot;PS $($MyInvocation.InvocationName)\&quot;
[string] $private:Message= \&quot;Environnement parent`r`n{0}\&quot; -F $(\&quot;-\&quot; * 40)
[System.Diagnostics.Debugger]::Log($level, $category, $message)

$private:Tab=Get-Variable -scope 1|? {$_.options -notmatch \&quot;AllScope\&quot;}|% {$_.name+ \&quot; : \&quot; + $_.Options}
ForEach( $Line in $private:Tab)
{[System.Diagnostics.Debugger]::Log($private:level, $private:category, $Line)}

[System.Diagnostics.Debugger]::Log($private:level, $private:category, $(\&quot;{0}\&quot; -F $(\&quot;-\&quot; * 40)) )


#Création de l'objet Closure, combinant le scriptbloc reçu en paramètre et l'environnement d'exécution.
$private:closure =
New-Object 'PSObject' |
Add-Member 'Script' $script -MemberType 'NoteProperty' -PassThru |
Add-Member 'Environment' $environment -MemberType 'NoteProperty' -PassThru;
Write-debug (\&quot;Closure: {0}\&quot; -F $private:closure)

# Mémorisation dans le magasin dédié de la fermeture.
#Le GUID est la clé qui désigne la fermeture, passée en tant une référence faible (weak reference)
$private:closureId = [Guid]::NewGuid();
$ClosureStore.Add($closureId, [WeakReference]$closure);

#Construit l'appel de la fermeture, on y insére le GUID dynamiquement
#Ex: InvokeClosureScript \&quot;903eafc7-10c4-4541-98d0-ff90f68071de\&quot; $Args;
$private:invokerText = \&quot;InvokeClosureScript `\&quot;$closureId`\&quot; `$Args;\&quot;;
Write-debug (\&quot;invokerText : {0}\&quot; -F $private:invokerText)

#On construit un scriptblock, tout en castant le résultat dans le type PSObject
#Ce scriptblock exécutera le traitement réel
#$private:invoker = [PSObject](Invoke-Expression \&quot;{ $invokerText }\&quot;«»);
$private:invoker=[PSObject]$ExecutionContext.InvokeCommand.NewScriptBlock($invokerText)

#On ajoute à l'objet invoker un membre nommé Closure qui contient l'objet personnalisé $Closure
#La durée de vie du script et de l'environnement est dépendant du fait que l'environnement est attaché au script
Add-Member -InputObject $invoker -Name 'Closure' -Value $closure -MemberType 'NoteProperty';
Write-debug (\&quot;invoker : {0}\&quot; -F $private:invoker)
return $invoker;
}

function global:InvokeClosureScript
{ #Exécute la varaible closure
param ([Guid]$private:closureId, [Array]$private:Args_)

#On récupére une référence faible sur la fermeture hébergée dans le magasin ($ClosureStore)
$private:closure = $ClosureStore[$closureId].Target;
Write-debug (\&quot;Closure : {0}\&quot; -F $private:closure)
if ( $DebugPreference -ne \&quot;SilentlyContinue\&quot;«»)
{ \&quot;Variable de contexte avant exécution de la closure\&quot;
$private:closure.Environment
}

if ($null -eq $closure) { throw 'The closure which in ID which it appoints relation is attached does not exist.'; }

#L'appel de fonction est construit dynamiquement.
#On déclare la clause param avec 2 arguments :
# -le script à exécuter, la closure,
# -et un tableau d'arguments (de la closure).
$private:invokerText = 'param ([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script';
Write-debug (\&quot;invokerText : {0}\&quot; -F $private:invokerText)


#On ajoute les possibles paramètres, récupérés dans le tableau Args_ de la clause Param précédente
for ($private:i = 0; $i -lt $Args_.Length; $i++) { $invokerText += \&quot; `$Args_[$i]\&quot;; }
Write-debug (\&quot;invokerText : {0}\&quot; -F $private:invokerText)

#On convertit le texte en un script
# exemple : param([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script
# param([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script $Args_[0]
# param([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script $Args_[0] $Args_[n]
#$private:invoker = Invoke-Expression \&quot;{ $invokerText }\&quot;;
#On construit un scriptblock avec le texte contenant du code
$private:invoker=$ExecutionContext.InvokeCommand.NewScriptBlock($invokerText)
Write-debug (\&quot;invoker : {0}\&quot; -F $private:invoker)
#Invoker contient quelque chose comme ceci, les lignes sont récréées :
# {
# param([ScriptBlock]$private:«»script, [Array]$private:Args_)
# .$script $Args_[0] $Args_[n]
# }

#On crée un scriptblock afin de créer une portée enfant.
#On charge l'environnement et on exécute la fermeture dans la portée enfant du ScriptBlock
#Il retourne le résulat de l'exécution de la commande passé dans le paramètre 1 ($Arg[0] dans le code)
#cette commande reçoit les paramètres
$private:result =
&amp;{
#$Args[0] référence le contenu de la variable invoker (scriptblock)
Write-debug (\&quot;`$Args[0]={0} \&quot; -F $Args[0])
#$Args[1] référence la fermeture
Write-debug (\&quot;`$Args[1].script={0}\&quot; -F $($Args[1].Script))
#$Args[1] référence les paramètres, optionnels, du code de la fermeture
Write-debug (\&quot;`$Args[2]={0}\&quot; -F $Args[2])

#Charge l'environnement dans la portée courante, c'est à dire ce scriptblock où on exécute la closure
#On recrée les objet variables qui ont été précédement sauvegardées
$Args[1].Environment | % { trap { continue; } $ExecutionContext.SessionState.PSVariable.Set($_); };
#Execute la fermeture avec le bon environnement(variables)
# et renvoi le résultat du traitement effectué par la closure
return .$Args[0] $Args[1].Script $Args[2];
} $invoker $closure $Args_;
#Résumée des portées :
#
# niveau 0 la function InvokeClosureScript
# niveau 1 le scriptblock Result : Restauration des variables
# niveau 2 le scriptblock de l'invoker : simule la portée parente de la closure
# niveau 3 le scriptblock de la closure : exécute le code de la closure
Write-debug (\&quot;Result= {0}\&quot; -F $result)
if ( $DebugPreference -ne \&quot;SilentlyContinue\&quot;«»)
{ \&quot;Variable de contexte après exécution de la closure\&quot;
$private:ColorTmp=$Host.Ui.RawUi.ForeGroundColor
$Host.Ui.RawUi.ForeGroundColor =\&quot;green\&quot;
$private:closure.Environment
$Host.Ui.RawUi.ForeGroundColor =$private:ColorTmp
}
return $result;
}

[/code:1]

La pièce jointe Closure3.ps1 est absente ou indisponible


Tutoriels PowerShell
Pièces jointes :

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 11 ans 9 mois #2970 par Robin
Aïe, claquage de cerveau !

Robin MVP PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 11 ans 9 mois #2974 par Laurent Dardenne
Robin écrit:

Aïe, claquage de cerveau !

Mdr :laugh:

Vous savez avec la V2 faut vous attendre à trouver sur sur le net des trucs carabinés.
Plus il y a de mou plus on tire sur la corde :side:

A ne pas confondre avec plus du pédale moins fort moins d'avance plus vite

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Temps de génération de la page : 0.325 secondes
Propulsé par Kunena