65.9K
CodeProject 正在变化。 阅读更多。
Home

从 COBOL 调用 F# 并返回

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2010年1月9日

CC (ASA 2.5)

11分钟阅读

viewsIcon

21110

演示 Micro Focus Managed COBOL 如何调用 F#,以及一些关于轻松混合命令式和函数式语言的技巧。

引言

在 .NET 上运行语言非常强大。使用托管 COBOL (来自 Micro Focus),可以利用 F# 代码来处理 COBOL 代码。想象一下,一个基于云的 F# MapReduce 系统正在消费遗留 COBOL 代码——是的,这真的指日可待。

目前,函数式语言受到了广泛关注。我猜测这背后的一大驱动力是函数式编程很容易映射到大规模并行和大规模分布式处理模型。由于云计算和多核技术的兴起,这些处理模型正变得越来越流行。从这个角度来看,值得注意的是,微软已将其 F# 函数式编程语言作为 Visual Studio 2010 核心 .NET 产品的一部分。

真正引起我兴趣的是,看看让 COBOL 程序与 F# 协同工作有多么容易。为此,我使用了一个尚未发布的 Micro Focus .NET COBOL 版本,运行在 Visual Studio 2010 中。这个项目非常有趣,它向我证明了让遗留或新的 COBOL 代码与 F# 互操作是一件非常实际的事情。

在上图的截图中,我们可以在 Visual Studio 2010 (beta) 中看到 F#,在下图中,可以看到同一 IDE 中的 COBOL。

设置测试场景

我不得不承认,我并不是 F# 的大师(甚至不是初学者),所以我不得不设计一个简单的 F# 示例,作为完整 MapReduce 或类似框架的最小模拟。我选择的方案是基于“Comparable”接口的一个简单的快速排序(即,它将对实现该接口的对象列表进行排序),以及一个递归函数,以用户可读的形式打印列表内容。您可以将这些视为分布式处理算法和分布式存储算法的模拟,就像云计算或网格计算所需要的那样。一旦实现了 COBOL 与这些技术轻松交互的方式,那么处理 MapReduce 将仅仅是扩展原理的问题。

关于类型的说明

函数式语言(尤其是 F#)在分布式编程方面表现出色,其中一个原因是它们非常适合处理不可变结构。在命令式语言(如 COBOL)中,我们倾向于使用可变结构;我们创建一个列表并向其中添加元素,或者创建一个整数变量并更新它。函数式编程提供了一种直观的方式来处理一旦创建就不会改变的结构;这些结构被称为不可变结构。

如果一个结构存在于分布式系统的多个节点上(或多核系统多个核心的缓存内存中),并且该结构被修改,那么修改必须传播到存储该结构的所有地方。这会给系统带来追踪和更新分布式结构的负担。然而,如果一个结构是不可变的,那么一旦分发,不同的节点或核心就可以在没有这种开销的情况下对其进行操作。

这一切的结果是 F# 在其语法中内置了许多不可变类型。就像许多函数式编程语言一样(例如 VSPL 是一个非常简单的例子:VSPL 索引页),F# 在其语法和语义结构中都深度集成了列表处理。[] 将创建一个空列表,而 [1,2,3,4,5] 将创建一个包含五个元素的列表。这些都是不可变列表。由于 F# 是一种 CLR (Common Language Runtime) 语言,它将这些不可变列表表示为 CLR (.NET) 类型。在这种情况下,它们实际上是 Microsoft.Microsoft.FSharp.Collections.FSharpList[type t] 泛型类。

对于 COBOL、C# 或 VB 来说,使用这些不可变类型是非常不自然的。因此,我在混合使用 COBOL 和 F# 时发现的第一件事是,在 F# 中编写小的类型转换函数确实很有帮助。例如,下面的函数将一个 System.Collections.Generic.List 转换为 F# 的不可变列表。

let toList seq =
     Seq.toList seq

Which is called from COBOL thus:

01 flist type Microsoft.FSharp.Collections.FSharpList[type COBOLFSharp.CustomerRecord].
...
set flist to  type ComparableQuickSort::toList(testList)

在示例 COBOL 程序中,我实际上不必明确指定 F# 风格的不可变列表的类型。

set flist to  type ComparableQuickSort::toList(testList)
set flist to  type ComparableQuickSort::quicksort(flist)
invoke  type ComparableQuickSort::print_list(flist)

但我也可以这样做

invoke type ComparableQuickSort::print_list
(
    type ComparableQuickSort::quicksort
    (
        type ComparableQuickSort::toList(testList)
    )
)

这使得 Micro Focus COBOL 编译器能够推断出传递的类型,并使用这些类型来创建泛型的正确具体类型。这表明编译器可以像类型推断的函数式编译器一样工作。事实上,这是否表明 MF 编译器已经变得多么复杂?

上面的代码还展示了如何从 COBOL 访问简单的 F# 函数。如果它们不返回任何值 (),我们可以使用 invoke 动词;如果它们返回一个值,我们可以使用 set 动词,或者在 invokeset 短语的参数列表中直接使用它们。F# 模块中的 F# 函数在 COBOL 中被视为同名类上的静态方法(类似于 VB 模块中的函数)。

F# 函数是如何工作的

我将尝试解释 F# 函数的作用;然而,我不是 F# 专家,所以这更像是 F# 101,而不是 F# 高级教程!

let rec print_list l =
     match l with
     | [] -> ()
     | h::t ->
           print_head h
           print_list t

好的,这使用 let 动词定义了一个名为 print_list 的函数。该函数通过 rec 动词是递归的。如果不存在 rev 动词,它就无法调用自身(它看起来调用自身的地方,实际上是在调用一个碰巧具有相同名称的新局部函数定义)。该函数接受一个列表作为参数,并且不返回任何内容。它逐个打印列表的内容,从第一个元素开始。

它通过“尾部递归”逐个元素地实现这一点。函数中的 match 动词将传递给函数的参数与不同的模式进行匹配。在这种情况下,有两种模式:要么是一个有元素的列表,要么是一个空列表。空列表的模式是 []。有元素的列表模式是 h::t。h::t 有一个特殊含义;它表示头部和尾部,其中列表的第一个元素是头部,任何剩余的元素都放在一个名为尾部的新列表中。因为我们调用 print_head h,所以头部元素每次都会被打印出来。因为我们调用 print_list t,任何其他元素都会被传递到函数中再次处理。当没有更多元素时,尾部就是一个空列表,匹配 [] 模式。这会停止递归并返回 ()() 表示无(一个没有元素的元组),这个无会一直传递回,直到成为原始调用的最终返回值。

在大多数命令式语言中,这种递归打印列表的定义效率会相当低。每次调用函数都需要将所有内容压入和弹出栈,这对于列表的每个元素都必须执行。在函数式编程语言中,编译器应该能够识别尾部递归的使用,并将递归函数编译成一个简单的循环。我实际上不知道 F# 编译器是否会这样做——但我真的很希望它会!

那么快速排序呢?

let rec quicksort l =
     match l with
     |[] -> []
     |h::t -> quicksort (List.filter ((<) h) t) @ [h] @ 
                    (quicksort (List.filter ((>=) h) t))

它的工作方式与 print_list 函数类似,都是使用递归。然而,它不使用尾部递归,而是使用围绕枢轴的列表拆分。头部元素被用作枢轴。传递列表的尾部被分成两个列表:一个列表中的所有元素都小于头部,另一个列表中的所有元素都大于或等于头部。然后将这两个列表传递给快速排序。子列表快速排序的结果被连接在头部两侧。这会产生一种树状递归,当传递的子列表为空时,每个分支都会开始展开。在这种情况下,它不像 print_list 那样返回无 (),而是返回一个空列表 [],这使得返回值的连接成为可能。

类型推断呢?

直到现在我才意识到,实际上有很多东西需要解释。难怪第一次成功让它工作起来就让我头疼。我认为这是“知道怎么做就很容易”的事情之一。

总之,在这两种情况下,编译器都可以推断出函数接受列表,因为它们使用列表运算符来处理传递的值。因此,编译器将参数类型设置为泛型列表。print_list 函数仅在匹配 [] 模式时从递归返回;此模式返回无 (),因此编译器“知道”该函数不返回值。在快速排序的情况下,它通过类似的推断“知道”返回值是一个列表。它还知道返回的列表将与传递的列表具有相同的具体类型,因为它是由相同的元素(只是顺序不同)构造的。

当从 COBOL 调用这些函数时,COBOL 编译器将它们视为 CLR 中的泛型类型。然后,它可以利用 COBOL 和 CLR 特定的规则来确定如何调用它们。由于它们是泛型的,调用参数和返回类型是安全的,但不需要在 F# 或 COBOL 中定义;它们是从调用参数的类型推断出来的。因此,F# 和 COBOL 的类型推断系统无缝协同工作——太棒了!

完整的 F# 示例

module ComparableQuickSort

let rec quicksort l =
     match l with
     |[] -> []
     |h::t -> quicksort (List.filter ((<) h) t) @ [h] 
                @ (quicksort (List.filter ((>=) h) t))

let rec print_head h = printfn "%A" h

let rec print_list l =
     match l with
     | [] -> ()
     | h::t ->
           print_head h
           print_list t

let toList seq =
     Seq.toList seq

现在是 COBOL

当我编写 COBOL 代码时,我脑海中有一个想法,认为这可能来自某个预先存在的仓库管理系统。想法是,“拣货”(有人去仓库取东西)被记录在某个 COBOL VSAM 文件中,我们想利用我们巧妙的 F# 框架在云环境中处理它们。

01 cust-grp property all.
03 pick-record-1 pic 9(4).
03 pick-record-2 pic x.
03 pick-sku      pic 9(8).

这个想法是,VSAM 文件将拣货记录存储在上图所示的组中。sku 是被拣货项的库存单位(唯一标识符)。record-1 是当天客户订单的唯一标识符,而 record-2 可以是 A 或 B。拣货总是先从主仓库尝试,这会产生一个 A 记录。如果物品在主仓库中找不到,那么会尝试从第二个仓库拣货,因此也会生成一个 B 记录。这些记录的排序首先按 record-1,然后按 sku,最后,B 记录必须在 A 记录之后。

为了实现这个逻辑,我创建了一个对象来封装该组。我们可以看到,我将 property all 应用于该组。这使得组的所有项都作为对象的属性公开。现在,遗留 COBOL 组的语义与 CLR 类型系统完全融合了!我通过让包含该组的类实现 Comparable 接口,在 COBOL 中实现了排序逻辑。

这个例子都是“虚构”的。如果我们用真实的遗留代码来做这件事,创建我们将传递给 F# 的对象的类就可以使用 Call 动词与遗留代码进行交互。这种方法将允许 .NET 逻辑充当遗留代码的薄包装器,使其能够与我们的新框架进行交互。

这个例子中实际程序的大部分内容只是将示例数据放入 CustomerRecord 对象!与 F# 交互的部分只是在最后一次返回之前的最后三行。

这一切意味着 COBOL 调用 F#,然后 F# 回调 COBOL!也就是说,CustomerRecord 对象(用 COBOL 编写)的 ToString()CompareTo 方法是从 F# 中调用的,而这些方法又是在从 COBOL 调用的函数中。这才是真正的语言互操作!

program-id. TestQuickSort as "COBOLFSharp.TestQuickSort".

data division.
working-storage section.
01 testList type 
 System.Collections.Generic.List[type COBOLFSharp.CustomerRecord].
01 cus-rec  type COBOLFSharp.CustomerRecord.
01 i binary-long.
01 y binary-long.
01 flist type 
 Microsoft.FSharp.Collections.FSharpList[type COBOLFSharp.CustomerRecord].
procedure division.
  set testList to 
      new System.Collections.Generic.List[type COBOLFSharp.CustomerRecord]
  perform varying i from 0 by 1 until i = 24
      compute y = (function mod( i 4)) + i * 10
      compute y = y  * 123 + y
      compute y = 11111111 b-xor y
      set cus-rec to new Type COBOLFSharp.CustomerRecord
      set cus-rec::pick-sku to y
      set cus-rec::pick-record-1 to i
      set cus-rec::pick-record-2 to "A"
      invoke testList::Add(cus-rec)
      if function mod(i 3) = 0 then
          set cus-rec to new Type COBOLFSharp.CustomerRecord
          set cus-rec::pick-sku to y
          set cus-rec::pick-record-1 to i
          set cus-rec::pick-record-2 to "B"
          invoke testList::Add(cus-rec)              
      end-if
  end-perform
  set flist to  type ComparableQuickSort::toList(testList)
  set flist to  type ComparableQuickSort::quicksort(flist)
  invoke  type ComparableQuickSort::print_list(flist)

  goback.
end program TestQuickSort.

class-id. CustomerRecord
as "COBOLFSharp.CustomerRecord"
implements type System.IComparable.

  working-storage section.
  01 cust-grp property all.
     03 pick-record-1 pic 9(4).
     03 pick-record-2 pic x.
     03 pick-sku      pic 9(8).

  method-id. CompareTo public.
  local-storage section.
     01 toCheck type COBOLFSharp.CustomerRecord.
  procedure division using by value toWhat 
     as object returning result as binary-long.
     *> Anything which is the wrong type is assumed equals (logical absurdity)
     if not toWhat instance of type COBOLFSharp.CustomerRecord then
         move 0 to result
         goback
     end-if
     set toCheck to toWhat as type COBOLFSharp.CustomerRecord
     if self::pick-record-1 > toCheck::pick-record-1 then
         move 1 to result
         goback
     end-if
     if self::pick-record-1 < toCheck::pick-record-1 then
         move -1 to result
         goback
     end-if
     *> Same pick - order by sku
     if self::pick-sku > toCheck::pick-sku then
         move 1 to result
         goback
     end-if
     if self::pick-sku < toCheck::pick-sku then
         move -1 to result
         goback
     end-if
     *> Move all b records to be after all a records
     if self::pick-record-2 = toCheck::pick-record-2 then
         move 0 to result
         goback
     end-if
     if self::pick-record-2 = "A" then
         move 1 to result
         goback
     end-if
    
     move -1 to result
     goback      
  end method.
 
  method-id. ToString override.
  procedure division returning stringVal as string.
      set stringVal to 
      String::Format("{0}/{1}/{2}" pick-record-1 pick-sku pick-record-2)
  end method.

end class.

结论

从 COBOL 调用 F# 是非常可行的。它确实需要一点时间来适应,我建议花时间设计两种语言之间的“接触点”,使其易于使用。例如,仅仅实现 toList 函数就让编程变得容易得多。如果能考虑到这一点,我认为 F# 和 COBOL 可以很好地协同工作,各自发挥其互补的优势。

© . All rights reserved.